plugnz 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/plugnz.mjs +20000 -0
  2. package/package.json +13 -10
  3. package/bin/plgnz.mjs +0 -3
  4. package/bin/plugnz.mjs +0 -5
  5. package/src/cli.ts +0 -489
  6. package/src/compatibility.ts +0 -23
  7. package/src/consumer-profiles.ts +0 -77
  8. package/src/conversion.ts +0 -544
  9. package/src/doctor.ts +0 -277
  10. package/src/exec.ts +0 -91
  11. package/src/fingerprint.ts +0 -77
  12. package/src/globals.d.ts +0 -97
  13. package/src/hermes-identity.ts +0 -4
  14. package/src/host.ts +0 -131
  15. package/src/hosts/claude-code-writer.ts +0 -589
  16. package/src/hosts/claude-code.ts +0 -124
  17. package/src/hosts/codex-writer.ts +0 -418
  18. package/src/hosts/codex.ts +0 -183
  19. package/src/hosts/cursor-writer.ts +0 -279
  20. package/src/hosts/cursor.ts +0 -107
  21. package/src/hosts/dcode-writer.ts +0 -150
  22. package/src/hosts/dcode.ts +0 -74
  23. package/src/hosts/grok-writer.ts +0 -254
  24. package/src/hosts/grok.ts +0 -94
  25. package/src/hosts/hermes-writer.ts +0 -223
  26. package/src/hosts/hermes.ts +0 -88
  27. package/src/hosts/index.ts +0 -25
  28. package/src/hosts/kimi-writer.ts +0 -238
  29. package/src/hosts/kimi.ts +0 -140
  30. package/src/hosts/omp-writer.ts +0 -179
  31. package/src/hosts/omp.ts +0 -153
  32. package/src/hosts/opencode-writer.ts +0 -112
  33. package/src/hosts/opencode.ts +0 -38
  34. package/src/hosts/pi-writer.ts +0 -111
  35. package/src/hosts/pi.ts +0 -48
  36. package/src/hosts/writers.ts +0 -23
  37. package/src/hosts/zcode-cli-writer.ts +0 -239
  38. package/src/hosts/zcode-cli.ts +0 -195
  39. package/src/mcp-write.ts +0 -81
  40. package/src/mcp.ts +0 -161
  41. package/src/paths.ts +0 -101
  42. package/src/pin.ts +0 -135
  43. package/src/source.ts +0 -224
  44. package/src/state-write.ts +0 -25
  45. package/src/state.ts +0 -118
  46. package/src/update.ts +0 -230
@@ -1,223 +0,0 @@
1
- /** Hermes portable package plus native discovery/command companion lifecycle. */
2
- import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import type { AddOptions, HostWriter, InstalledPlugin, PinOptions, PinOutcome } from '../host';
6
- import type { PluginSource, ResolvedSource } from '../source';
7
- import { projectPluginForHermes } from '../conversion';
8
- import { hermesCommandCompanionId } from '../hermes-identity';
9
- import { hermes, hermesPluginsDir } from './hermes';
10
- import { hermesConfigPath, hermesRoot } from '../paths';
11
- import { pinPluginMcpFiles } from '../mcp-write';
12
-
13
- declare const Bun: { YAML: { parse(input: string): unknown; stringify(value: unknown): string } };
14
-
15
- const MARKER = '.plgnz-install.json';
16
- type Ownership = { source: string; pluginId: string; fingerprint: string };
17
- type Change = { commit(): void; rollback(): void };
18
-
19
- export const hermesWriter: HostWriter = {
20
- ...hermes,
21
- supportsAdoption: true,
22
- async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
23
- const id = plugin.name;
24
- const ownershipId = plugin.marketplace === undefined ? id : `${id}@${plugin.marketplace}`;
25
- const companionId = hermesCommandCompanionId(id);
26
- assertId(id); assertId(companionId);
27
- const root = hermesPluginsDir();
28
- const target = join(root, id); const companion = join(root, companionId);
29
- assertManagedPath(root, target); assertManagedPath(root, companion);
30
- effectiveConfigPath();
31
- if (!opts?.dryRun) mkdirSync(root, { recursive: true });
32
- const stageRoot = mkdtempSync(join(opts?.dryRun ? tmpdir() : root, '.plgnz-hermes-stage-'));
33
- const packageStage = join(stageRoot, 'package'); const companionStage = join(stageRoot, 'commands');
34
- mkdirSync(packageStage); mkdirSync(companionStage);
35
- try {
36
- projectPluginForHermes(plugin.dir, packageStage, companionStage);
37
- validatePackage(packageStage, id);
38
- const hasCompanion = readdirSync(companionStage).length > 0;
39
- if (hasCompanion) validateCompanion(companionStage, companionId);
40
- const ownership: Ownership = { source: resolved.sourceUri, pluginId: ownershipId, fingerprint: plugin.contentFingerprint ?? '' };
41
- writeFileSync(join(packageStage, MARKER), JSON.stringify(ownership));
42
- if (hasCompanion) writeFileSync(join(companionStage, MARKER), JSON.stringify(ownership));
43
- assertReplaceable(target, packageStage, id, ownershipId, resolved.sourceUri, opts?.adoptExisting === true, validatePackage);
44
- if (hasCompanion) assertReplaceable(companion, companionStage, companionId, ownershipId, resolved.sourceUri, opts?.adoptExisting === true, validateCompanion);
45
- else if (existsSync(companion)) assertReplaceable(companion, undefined, companionId, ownershipId, resolved.sourceUri, opts?.adoptExisting === true, validateCompanion);
46
- const primaryMarker = readOwnership(target); const companionMarker = readOwnership(companion);
47
- const unchangedPrimary = primaryMarker?.pluginId === ownershipId && primaryMarker.fingerprint === ownership.fingerprint && sameTree(packageStage, target);
48
- const unchangedCompanion = hasCompanion
49
- ? companionMarker?.pluginId === ownershipId && companionMarker.fingerprint === ownership.fingerprint && sameTree(companionStage, companion)
50
- : !existsSync(companion);
51
- if (opts?.dryRun) return unchangedPrimary && unchangedCompanion ? 'unchanged' : undefined;
52
- if (unchangedPrimary && unchangedCompanion) {
53
- updatePluginConfig([id, ...(hasCompanion ? [companionId] : [])], []);
54
- return 'unchanged';
55
- }
56
- const changes: Change[] = [];
57
- try {
58
- changes.push(replaceTarget(packageStage, target, root));
59
- if (hasCompanion) changes.push(replaceTarget(companionStage, companion, root));
60
- else if (existsSync(companion)) changes.push(removeTarget(companion, root));
61
- updatePluginConfig([id, ...(hasCompanion ? [companionId] : [])], hasCompanion ? [] : [companionId]);
62
- for (const change of changes) change.commit();
63
- } catch (error) {
64
- for (const change of changes.reverse()) change.rollback();
65
- throw error;
66
- }
67
- return;
68
- } finally { rmSync(stageRoot, { recursive: true, force: true }); }
69
- },
70
- async pin(plugin: InstalledPlugin, opts?: PinOptions): Promise<PinOutcome> {
71
- return plugin.path === undefined ? { changes: [], refusals: [] } : pinPluginMcpFiles(plugin.path, [{ kind: 'spec', file: 'mcp.json' }], opts);
72
- },
73
- async remove(id: string): Promise<void> {
74
- const nativeId = nativePluginId(id);
75
- const root = hermesPluginsDir(); const target = join(root, nativeId); const companionId = hermesCommandCompanionId(nativeId); const companion = join(root, companionId);
76
- assertManagedPath(root, target); assertManagedPath(root, companion);
77
- effectiveConfigPath();
78
- const marker = readOwnership(target);
79
- if (marker === null || marker.pluginId !== id) return;
80
- const changes: Change[] = [];
81
- try {
82
- if (existsSync(target)) changes.push(removeTarget(target, root));
83
- const companionMarker = readOwnership(companion); const disable = [nativeId];
84
- if (companionMarker !== null && companionMarker.pluginId === marker.pluginId && companionMarker.source === marker.source && companionMarker.fingerprint === marker.fingerprint) { changes.push(removeTarget(companion, root)); disable.push(companionId); }
85
- else if (!existsSync(companion)) disable.push(companionId);
86
- updatePluginConfig([], disable);
87
- for (const change of changes) change.commit();
88
- } catch (error) {
89
- for (const change of changes.reverse()) change.rollback();
90
- throw error;
91
- }
92
- },
93
- };
94
-
95
- function assertReplaceable(target: string, stage: string | undefined, expected: string, ownershipId: string, source: string, adopt: boolean, validate: (root: string, expected: string) => void): void {
96
- if (!existsSync(target)) return;
97
- const marker = readOwnership(target);
98
- if (marker !== null) {
99
- const packageId = expected.replace(/\.plgnz-commands$/u, '');
100
- if (marker.source !== source || !matches(marker.pluginId, packageId, ownershipId)) throw new Error(`Hermes plugin ${expected} belongs to another source; refusing to replace it`);
101
- return;
102
- }
103
- if (stage !== undefined && sameTree(stage, target)) return;
104
- if (!adopt) throw new Error(`Hermes plugin ${expected} is unowned and differs from the staged representation; pass --adopt-existing to take ownership explicitly`);
105
- validate(target, expected);
106
- }
107
-
108
- function validatePackage(root: string, expected: string): void {
109
- assertTree(root);
110
- const manifest = join(root, 'plugin.json');
111
- if (!existsSync(manifest)) throw new Error('Hermes stage has no Agent Plugins manifest');
112
- let value: unknown;
113
- try { value = JSON.parse(readFileSync(manifest, 'utf8')); } catch (error) { throw new Error(`invalid Hermes plugin manifest: ${(error as Error).message}`); }
114
- if (!value || typeof value !== 'object' || Array.isArray(value) || (value as Record<string, unknown>).name !== expected) throw new Error(`Hermes manifest identity does not match ${expected}`);
115
- if ((value as Record<string, unknown>)['$schema'] !== 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json') throw new Error(`Hermes portable manifest must use the Agent Plugins v1 schema: ${manifest}`);
116
- }
117
-
118
- function validateCompanion(root: string, expected: string): void {
119
- assertTree(root);
120
- const manifest = join(root, 'plugin.yaml');
121
- if (!existsSync(manifest) || !existsSync(join(root, '__init__.py'))) throw new Error(`Hermes native companion is incomplete: ${root}`);
122
- const value: unknown = Bun.YAML.parse(readFileSync(manifest, 'utf8'));
123
- if (!value || typeof value !== 'object' || Array.isArray(value) || (value as Record<string, unknown>).name !== expected) throw new Error(`Hermes companion identity does not match ${expected}`);
124
- }
125
-
126
- function readOwnership(target: string): Ownership | null {
127
- const file = join(target, MARKER); if (!existsSync(file)) return null;
128
- try {
129
- const value: unknown = JSON.parse(readFileSync(file, 'utf8'));
130
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('marker must be an object');
131
- const row = value as Record<string, unknown>;
132
- if (typeof row.source !== 'string' || typeof row.pluginId !== 'string' || typeof row.fingerprint !== 'string') throw new Error('marker fields are invalid');
133
- return row as Ownership;
134
- } catch (error) { throw new Error(`invalid Hermes ownership marker: ${file} (${(error as Error).message})`); }
135
- }
136
-
137
- function replaceTarget(stage: string, target: string, root: string): Change {
138
- if (!existsSync(target)) { renameSync(stage, target); return { commit: () => {}, rollback: () => rmSync(target, { recursive: true, force: true }) }; }
139
- const backupRoot = mkdtempSync(join(root, '.plgnz-hermes-backup-')); const backup = join(backupRoot, 'previous');
140
- renameSync(target, backup);
141
- try { renameSync(stage, target); } catch (error) { renameSync(backup, target); rmSync(backupRoot, { recursive: true, force: true }); throw error; }
142
- return { commit: () => rmSync(backupRoot, { recursive: true, force: true }), rollback: () => { rmSync(target, { recursive: true, force: true }); renameSync(backup, target); rmSync(backupRoot, { recursive: true, force: true }); } };
143
- }
144
-
145
- function removeTarget(target: string, root: string): Change {
146
- const backupRoot = mkdtempSync(join(root, '.plgnz-hermes-remove-')); const backup = join(backupRoot, 'previous');
147
- renameSync(target, backup);
148
- return { commit: () => rmSync(backupRoot, { recursive: true, force: true }), rollback: () => { if (!existsSync(target)) renameSync(backup, target); rmSync(backupRoot, { recursive: true, force: true }); } };
149
- }
150
-
151
- function updatePluginConfig(enable: string[], disable: string[]): void {
152
- const configured = effectiveConfigPath();
153
- const actual = existsSync(configured) && lstatSync(configured).isSymbolicLink() ? realpathSync(configured) : configured;
154
- const exists = existsSync(actual); const original = exists ? readFileSync(actual, 'utf8') : '';
155
- let parsed: Record<string, unknown> = {};
156
- if (original.trim() !== '') {
157
- const value: unknown = Bun.YAML.parse(original);
158
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Hermes config is not a mapping: ${configured}`);
159
- parsed = value as Record<string, unknown>;
160
- }
161
- const pluginsValue = parsed['plugins'];
162
- if (pluginsValue !== undefined && pluginsValue !== null && (typeof pluginsValue !== 'object' || Array.isArray(pluginsValue))) throw new Error(`Hermes plugins config is not a mapping: ${configured}`);
163
- const plugins = (pluginsValue ?? {}) as Record<string, unknown>;
164
- const list = (key: 'enabled' | 'disabled'): string[] => {
165
- const value = plugins[key];
166
- if (value === undefined || value === null) return [];
167
- if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) throw new Error(`Hermes plugins.${key} is not a string list: ${configured}`);
168
- return [...new Set(value as string[])];
169
- };
170
- const enabled = new Set(list('enabled')); const disabled = new Set(list('disabled'));
171
- for (const id of enable) { enabled.add(id); disabled.delete(id); }
172
- for (const id of disable) { enabled.delete(id); disabled.add(id); }
173
- plugins['enabled'] = [...enabled].sort(); plugins['disabled'] = [...disabled].sort(); parsed['plugins'] = plugins;
174
- const next = Bun.YAML.stringify(parsed);
175
- const verified: unknown = Bun.YAML.parse(next);
176
- if (!verified || typeof verified !== 'object' || Array.isArray(verified)) throw new Error(`Hermes config serialization failed: ${configured}`);
177
- const verifiedPlugins = (verified as Record<string, unknown>)['plugins'];
178
- if (!verifiedPlugins || typeof verifiedPlugins !== 'object' || Array.isArray(verifiedPlugins) ||
179
- JSON.stringify((verifiedPlugins as Record<string, unknown>)['enabled']) !== JSON.stringify(plugins['enabled']) ||
180
- JSON.stringify((verifiedPlugins as Record<string, unknown>)['disabled']) !== JSON.stringify(plugins['disabled'])) {
181
- throw new Error(`Hermes config serialization changed plugin activation: ${configured}`);
182
- }
183
- mkdirSync(dirname(actual), { recursive: true });
184
- const temp = join(dirname(actual), `.plgnz-hermes-config-${Date.now()}-${Math.random().toString(16).slice(2)}.yaml`);
185
- try {
186
- writeFileSync(temp, next);
187
- if (exists) chmodSync(temp, (statSync(actual) as unknown as { mode: number }).mode & 0o777);
188
- renameSync(temp, actual);
189
- } finally { rmSync(temp, { force: true }); }
190
- }
191
-
192
- function effectiveConfigPath(): string {
193
- const native = resolve(join(hermesRoot(), 'config.yaml'));
194
- const configured = resolve(hermesConfigPath());
195
- if (native === configured) return configured;
196
- if (!existsSync(native) || !existsSync(configured)) throw new Error(`Hermes only reads ${native}; configured path ${configured} must already resolve to the same file`);
197
- const nativeReal = realpathSync(native); const configuredReal = realpathSync(configured);
198
- if (nativeReal === configuredReal) return configured;
199
- throw new Error(`Hermes only reads ${native}; configured path ${configured} is a different file`);
200
- }
201
-
202
- function sameTree(left: string, right: string): boolean {
203
- if (!existsSync(right)) return false;
204
- const list = (root: string): string[] => {
205
- const out: string[] = [];
206
- const walk = (dir: string, prefix: string): void => { for (const name of readdirSync(dir).sort()) { if (name === MARKER) continue; const path = join(dir, name), relative = prefix ? `${prefix}/${name}` : name, stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`Hermes plugin contains symlink: ${path}`); if (stat.isDirectory()) walk(path, relative); else if (stat.isFile()) out.push(`${relative}:${Array.from(readBytes(path)).join(',')}`); else throw new Error(`Hermes plugin contains unsupported file: ${path}`); } };
207
- walk(root, ''); return out;
208
- };
209
- return JSON.stringify(list(left)) === JSON.stringify(list(right));
210
- }
211
-
212
- function readBytes(path: string): Uint8Array { return (readFileSync as unknown as (file: string) => Uint8Array)(path); }
213
- function assertTree(root: string): void { const stat = lstatSync(root); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Hermes plugin source is not a real directory: ${root}`); for (const name of readdirSync(root)) { if (name === MARKER) continue; const path = join(root, name), entry = lstatSync(path); if (entry.isSymbolicLink()) throw new Error(`Hermes plugin contains symlink: ${path}`); if (entry.isDirectory()) assertTree(path); else if (!entry.isFile()) throw new Error(`Hermes plugin contains unsupported entry: ${path}`); } }
214
- function assertManagedPath(root: string, target: string): void { const base = resolve(root), selected = resolve(target); if (selected !== base && !selected.startsWith(`${base}/`)) throw new Error(`Hermes managed path escapes its store: ${target}`); let current = base; for (const part of selected.slice(base.length).split('/').filter(Boolean)) { if (existsSync(current)) { const stat = lstatSync(current); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Hermes managed path component is unsafe: ${current}`); } current = join(current, part); } if (existsSync(current)) { const stat = lstatSync(current); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Hermes managed path component is unsafe: ${current}`); } }
215
- function assertId(id: string): void { if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(id)) throw new Error(`unsafe Hermes plugin identity: ${id}`); }
216
- function nativePluginId(logicalId: string): string {
217
- const parts = logicalId.split('@');
218
- if (parts.length > 2 || parts[0] === undefined || parts[0] === '') throw new Error(`unsafe Hermes plugin identity: ${logicalId}`);
219
- assertId(parts[0]);
220
- if (parts[1] !== undefined) assertId(parts[1]);
221
- return parts[0];
222
- }
223
- function matches(marker: string, id: string, ownership: string): boolean { return marker === id || marker === ownership; }
@@ -1,88 +0,0 @@
1
- /**
2
- * Hermes portable Agent Plugin reader.
3
- *
4
- * Evidence: Hermes Agent `c0d7294769`, `hermes_cli/plugins_cmd.py` accepts a
5
- * root `plugin.json` under `$HERMES_HOME/plugins`; portable packages are active
6
- * only when their native name is in `plugins.enabled` and absent from
7
- * `plugins.disabled`.
8
- */
9
- import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
- import { join } from 'node:path';
11
- import type { HostReader, InstalledPlugin, McpServerEntry } from '../host';
12
- import { hermesRoot } from '../paths';
13
- import { collectPluginServers } from '../mcp';
14
- import { hermesCommandCompanionId } from '../hermes-identity';
15
-
16
- declare const Bun: { YAML: { parse(input: string): unknown } };
17
-
18
- export function hermesPluginsDir(): string { return join(hermesRoot(), 'plugins'); }
19
-
20
- type Manifest = { name: string; version?: string };
21
-
22
- function ownedId(dir: string, fallback: string): string {
23
- const file = join(dir, '.plgnz-install.json');
24
- if (!existsSync(file)) return fallback;
25
- try {
26
- const value: unknown = JSON.parse(readFileSync(file, 'utf8'));
27
- if (!value || typeof value !== 'object' || Array.isArray(value)) return fallback;
28
- const id = (value as Record<string, unknown>).pluginId;
29
- return typeof id === 'string' && (id === fallback || id.startsWith(`${fallback}@`)) ? id : fallback;
30
- } catch { return fallback; }
31
- }
32
-
33
- function readManifest(dir: string): Manifest | undefined {
34
- const file = join(dir, 'plugin.json');
35
- if (!existsSync(file) || !statSync(file).isFile()) return undefined;
36
- try {
37
- const value: unknown = JSON.parse(readFileSync(file, 'utf8'));
38
- if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
39
- const record = value as Record<string, unknown>;
40
- if (typeof record.name !== 'string' || !validId(record.name)) return undefined;
41
- if (record.version !== undefined && typeof record.version !== 'string') return undefined;
42
- return { name: record.name, ...(typeof record.version === 'string' ? { version: record.version } : {}) };
43
- } catch { return undefined; }
44
- }
45
-
46
- function names(key: 'enabled' | 'disabled'): Set<string> {
47
- const config = join(hermesRoot(), 'config.yaml');
48
- if (!existsSync(config)) return new Set();
49
- try {
50
- const parsed: unknown = Bun.YAML.parse(readFileSync(config, 'utf8'));
51
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return new Set();
52
- const plugins = (parsed as Record<string, unknown>)['plugins'];
53
- if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins)) return new Set();
54
- const values = (plugins as Record<string, unknown>)[key];
55
- return new Set(Array.isArray(values) ? values.filter((value): value is string => typeof value === 'string' && validId(value)) : []);
56
- } catch { return new Set(); }
57
- }
58
-
59
- function validId(value: string): boolean { return /^[a-z0-9][a-z0-9._-]*$/iu.test(value); }
60
-
61
- export const hermes: HostReader = {
62
- id: 'hermes', gui: false,
63
- detect: () => existsSync(join(hermesRoot(), 'config.yaml')) || existsSync(hermesPluginsDir()),
64
- stores: () => [hermesRoot(), hermesPluginsDir()],
65
- listInstalled(): InstalledPlugin[] {
66
- const root = hermesPluginsDir();
67
- if (!existsSync(root)) return [];
68
- const enabled = names('enabled'); const disabled = names('disabled'); const result: InstalledPlugin[] = [];
69
- for (const entry of readdirSync(root).sort()) {
70
- if (entry.startsWith('.')) continue;
71
- const dir = join(root, entry);
72
- if (lstatSync(dir).isSymbolicLink() || !statSync(dir).isDirectory()) continue;
73
- const manifest = readManifest(dir);
74
- if (manifest === undefined) continue;
75
- const id = ownedId(dir, manifest.name); const at = id.indexOf('@');
76
- const companion = join(root, hermesCommandCompanionId(manifest.name));
77
- const hasCompanion = existsSync(join(companion, 'plugin.yaml'));
78
- const companionId = hermesCommandCompanionId(manifest.name);
79
- const contentRoots: Record<string, string> = hasCompanion ? { package: dir, commands: companion } : { package: dir };
80
- const active = enabled.has(manifest.name) && !disabled.has(manifest.name) && (!hasCompanion || (enabled.has(companionId) && !disabled.has(companionId)));
81
- result.push({ id, name: manifest.name, ...(at < 0 ? {} : { marketplace: id.slice(at + 1) }), path: dir, contentRoots, ...(manifest.version ? { version: manifest.version } : {}), enabled: active });
82
- }
83
- return result;
84
- },
85
- mcpEntries(): McpServerEntry[] {
86
- return this.listInstalled().flatMap(plugin => plugin.path === undefined ? [] : collectPluginServers(plugin.id, plugin.path, [{ kind: 'spec', file: 'mcp.json' }]));
87
- },
88
- };
@@ -1,25 +0,0 @@
1
- /**
2
- * Host registry — readers only.
3
- *
4
- * doctor imports `hosts` from here, and doctor is read-only by construction
5
- * (AGENTS.md: it must not import any writer). A module is evaluated whole,
6
- * so honoring that at module granularity means writers cannot live in the
7
- * reader files: each host's writer is a sibling `<host>-writer.ts`, and the
8
- * writer list is aggregated in src/hosts/writers.ts — a module this index
9
- * never imports. test/doctor-imports.test.ts walks doctor's import graph and
10
- * fails if a writer creeps back into it.
11
- */
12
- import type { HostReader } from '../host';
13
- import { claudeCode } from './claude-code';
14
- import { codex } from './codex';
15
- import { kimi } from './kimi';
16
- import { cursor } from './cursor';
17
- import { omp } from './omp';
18
- import { pi } from './pi';
19
- import { dcode } from './dcode';
20
- import { opencode } from './opencode';
21
- import { grok } from './grok';
22
- import { zcodeCli } from './zcode-cli';
23
- import { hermes } from './hermes';
24
-
25
- export const hosts: HostReader[] = [claudeCode, codex, kimi, cursor, omp, hermes, pi, dcode, opencode, grok, zcodeCli];
@@ -1,238 +0,0 @@
1
- /** Transactional native Kimi Code plugin writer. */
2
- import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
- import { dirname, isAbsolute, join, resolve } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import type { AddOptions, HostWriter, InstalledPlugin, PinOptions, PinOutcome } from '../host';
6
- import type { PluginSource, ResolvedSource } from '../source';
7
- import { normalizeCommandSources } from '../conversion';
8
- import { kimi, mcpCandidates, pluginsDir } from './kimi';
9
- import { pinPluginMcpFiles } from '../mcp-write';
10
-
11
- declare const Bun: any;
12
- declare const TextDecoder: any;
13
- declare const Response: any;
14
-
15
- const MARKER = '.plgnz-install.json';
16
- type Ownership = { source: string; pluginId: string; fingerprint: string };
17
- type Registry = { version: 1; plugins: Array<Record<string, unknown>> };
18
-
19
- export const kimiWriter: HostWriter = {
20
- ...kimi,
21
- supportsAdoption: true,
22
- async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
23
- const id = plugin.name;
24
- const ownedId = plugin.marketplace === undefined ? id : `${id}@${plugin.marketplace}`;
25
- assertId(id);
26
- const root = pluginsDir();
27
- const managedRoot = join(root, 'managed');
28
- const target = join(managedRoot, id);
29
- const registryFile = join(root, 'installed.json');
30
- assertManagedPath(kimiRootPath(), root);
31
- assertManagedPath(kimiRootPath(), managedRoot);
32
- assertManagedPath(kimiRootPath(), target);
33
- assertFilePath(kimiRootPath(), registryFile);
34
- const stageRoot = mkdtempSync(join(opts?.dryRun ? tmpdir() : (mkdirSync(root, { recursive: true }), root), '.plgnz-kimi-stage-'));
35
- const stage = join(stageRoot, id);
36
- try {
37
- stagePlugin(plugin.dir, stage, id);
38
- writeFileSync(join(stage, MARKER), JSON.stringify({ source: resolved.sourceUri, pluginId: ownedId, fingerprint: plugin.contentFingerprint ?? '' } satisfies Ownership));
39
- const prior = readRegistry(registryFile);
40
- const priorRow = prior.plugins.find(row => row.id === id);
41
- const marker = readOwnership(target);
42
- const markerHasExpectedIdentity = marker?.pluginId === ownedId || (plugin.marketplace !== undefined && marker?.pluginId === id);
43
- if (marker !== null && (marker.source !== resolved.sourceUri || !markerHasExpectedIdentity)) throw new Error(`Kimi plugin ${id} belongs to another source; refusing to replace it`);
44
- const same = existsSync(target) && sameTree(stage, target);
45
- if (existsSync(target) && marker === null && !same) {
46
- if (!opts?.adoptExisting) throw new Error(`Kimi managed plugin ${id} is unowned and differs from the staged representation; pass --adopt-existing to take ownership explicitly`);
47
- validateNativeIdentity(target, id);
48
- }
49
- const unchanged = marker !== null && marker.fingerprint === (plugin.contentFingerprint ?? '') && same && priorRow?.enabled === true && sameRoot(priorRow.root, target);
50
- if (opts?.dryRun) {
51
- console.log(`[kimi] would install and enable staged plugin: ${target}`);
52
- console.log(`[kimi] would let Kimi update registry: ${registryFile}`);
53
- return unchanged ? 'unchanged' : undefined;
54
- }
55
- if (unchanged) return 'unchanged';
56
-
57
- // Kimi's documented API removes its existing managed copy during re-install.
58
- // Keep both active bytes and registry aside until its native readback succeeds.
59
- // Capture all rollback bytes before renaming the active tree. A registry
60
- // read failure must leave the old target in place.
61
- const registryBefore = existsSync(registryFile) ? readFileSync(registryFile, 'utf8') : undefined;
62
- const backup = moveAside(target, managedRoot);
63
- try {
64
- await nativeInstall(stage, kimiRootPath(), resolveKimiBinary());
65
- const after = readRegistry(registryFile);
66
- const row = after.plugins.find(candidate => candidate.id === id);
67
- if (row === undefined || row.enabled !== true || !sameRoot(row.root, target) || !existsSync(target) || !sameTree(stage, target)) {
68
- throw new Error(`Kimi native install did not produce enabled managed plugin ${id}`);
69
- }
70
- } catch (error) {
71
- rmSync(target, { recursive: true, force: true });
72
- backup.rollback();
73
- restore(registryFile, registryBefore);
74
- throw error;
75
- }
76
- // Native registry now selects the new tree. Cleanup is deliberately after
77
- // that commit: its failure must never roll back a working installation.
78
- backup.commit();
79
- } finally {
80
- rmSync(stageRoot, { recursive: true, force: true });
81
- }
82
- },
83
- async pin(plugin: InstalledPlugin, opts?: PinOptions): Promise<PinOutcome> {
84
- if (plugin.path === undefined || !existsSync(plugin.path)) return { changes: [], refusals: [] };
85
- return pinPluginMcpFiles(plugin.path, mcpCandidates(), opts);
86
- },
87
- async remove(id: string): Promise<void> {
88
- const nativeId = id.split('@', 1)[0] ?? '';
89
- assertId(nativeId);
90
- const root = pluginsDir(); const target = join(root, 'managed', nativeId); const registryFile = join(root, 'installed.json');
91
- assertManagedPath(kimiRootPath(), root); assertManagedPath(kimiRootPath(), dirname(target)); assertManagedPath(kimiRootPath(), target); assertFilePath(kimiRootPath(), registryFile);
92
- const row = readRegistry(registryFile).plugins.find(candidate => candidate.id === nativeId);
93
- const marker = readOwnership(target);
94
- if (row === undefined || marker === null || marker.pluginId !== id || !sameRoot(row.root, target)) throw new Error(`Kimi plugin ${id} is not wholly plgnz-owned; refusing native removal`);
95
- await nativeRemove(kimiRootPath(), resolveKimiBinary(), nativeId);
96
- if (readRegistry(registryFile).plugins.some(candidate => candidate.id === nativeId)) throw new Error(`Kimi native removal did not deactivate plugin ${id}`);
97
- },
98
- };
99
-
100
- function kimiRootPath(): string { return dirname(pluginsDir()); }
101
- function assertId(value: string): void { if (!/^[a-z0-9][a-z0-9_-]{0,63}$/iu.test(value)) throw new Error(`invalid Kimi plugin id: ${value}`); }
102
- function sameRoot(value: unknown, target: string): boolean {
103
- if (typeof value !== 'string') return false;
104
- try {
105
- const left = statSync(value) as unknown as { dev: number; ino: number };
106
- const right = statSync(target) as unknown as { dev: number; ino: number };
107
- return left.dev === right.dev && left.ino === right.ino;
108
- }
109
- catch { return resolve(value) === resolve(target); }
110
- }
111
- function readRegistry(path: string): Registry {
112
- if (!existsSync(path)) return { version: 1, plugins: [] };
113
- let raw: unknown; try { raw = JSON.parse(readFileSync(path, 'utf8')); } catch (error) { throw new Error(`invalid Kimi installed registry: ${(error as Error).message}`); }
114
- if (!isObject(raw) || raw.version !== 1 || !Array.isArray(raw.plugins) || raw.plugins.some(row => !isObject(row) || typeof row.id !== 'string' || typeof row.root !== 'string' || typeof row.enabled !== 'boolean')) throw new Error(`unsupported Kimi installed registry: ${path}`);
115
- return raw as Registry;
116
- }
117
- function restore(path: string, value: string | undefined): void { if (value === undefined) rmSync(path, { force: true }); else writeFileSync(path, value); }
118
- function isObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); }
119
-
120
- function stagePlugin(source: string, stage: string, expectedName: string): void {
121
- assertNoSymlinks(source); cpSync(source, stage, { recursive: true });
122
- normalizeCommandSources(stage);
123
- const manifest = readSourceManifest(stage);
124
- if (manifest.name !== expectedName) throw new Error(`Kimi manifest identity does not match ${expectedName}`);
125
- for (const key of ['hooks', 'agents', 'agent', 'executables']) if (manifest[key] !== undefined) throw new Error(`Kimi root manifest ${key} is unsupported without an explicit native declaration`);
126
- const skills = join(stage, 'skills'); if (existsSync(skills) && !statSync(skills).isDirectory()) throw new Error(`Kimi skills path is not a directory: ${skills}`);
127
- if (existsSync(skills)) assertKimiSkillPolicies(skills);
128
- const native = join(stage, 'kimi.plugin.json');
129
- let supplied: Record<string, unknown> = {};
130
- if (existsSync(native)) {
131
- supplied = parseJson(native, 'Kimi native manifest');
132
- if (supplied.name !== undefined && supplied.name !== expectedName) throw new Error(`Kimi native manifest name conflicts with ${expectedName}`);
133
- if (supplied.version !== undefined && supplied.version !== manifest.version) throw new Error('Kimi native manifest version conflicts with Agent Plugins manifest');
134
- if (supplied.skills !== undefined && supplied.skills !== './skills/' && supplied.skills !== './skills') throw new Error(`unsupported Kimi native skills pointer: ${String(supplied.skills)}`);
135
- if (supplied.commands !== undefined && !['./commands/', './commands', './.claude/commands/', './.claude/commands'].includes(String(supplied.commands))) throw new Error(`unsupported Kimi native commands pointer: ${String(supplied.commands)}`);
136
- for (const key of Object.keys(supplied)) if (!['name', 'version', 'description', 'skills', 'commands', 'mcpServers'].includes(key)) throw new Error(`unsupported Kimi native manifest field: ${key}`);
137
- if (supplied.mcpServers !== undefined && !isObject(supplied.mcpServers)) throw new Error('Kimi native manifest mcpServers must be an object');
138
- }
139
- const commands = prepareCommands(stage, typeof supplied.commands === 'string' ? supplied.commands : undefined);
140
- const mcpServers = collectMcpServers(stage, manifest, supplied);
141
- const nativeManifest: Record<string, unknown> = { name: expectedName, ...(typeof manifest.version === 'string' ? { version: manifest.version } : {}), ...(typeof manifest.description === 'string' ? { description: manifest.description } : {}), ...(existsSync(skills) ? { skills: './skills/' } : {}), ...(commands !== undefined ? { commands } : {}), ...(mcpServers !== undefined ? { mcpServers } : {}) };
142
- writeFileSync(native, JSON.stringify(nativeManifest, null, 2));
143
- assertNoSymlinks(stage);
144
- }
145
- function collectMcpServers(stage: string, manifest: Record<string, unknown>, supplied: Record<string, unknown>): Record<string, unknown> | undefined {
146
- const merged: Record<string, unknown> = {};
147
- let found = false;
148
- const add = (label: string, value: unknown): void => {
149
- if (value === undefined) return;
150
- if (!isObject(value)) throw new Error(`Kimi ${label} mcpServers must be an object`);
151
- found = true;
152
- for (const [name, server] of Object.entries(value)) {
153
- if (!isObject(server)) throw new Error(`Kimi ${label} MCP server ${name} must be an object`);
154
- if (merged[name] !== undefined && JSON.stringify(merged[name]) !== JSON.stringify(server)) throw new Error(`Kimi MCP server ${name} conflicts between supported source declarations`);
155
- merged[name] = server;
156
- }
157
- };
158
- // A native declaration is already Kimi's active representation; canonical
159
- // source declarations must agree with it rather than being silently lost.
160
- add('native manifest', supplied.mcpServers);
161
- const legacyNative = join(stage, '.kimi-plugin', 'plugin.json');
162
- if (existsSync(legacyNative)) add('.kimi-plugin/plugin.json', parseJson(legacyNative, '.kimi-plugin/plugin.json').mcpServers);
163
- add('root plugin manifest', manifest.mcpServers);
164
- for (const file of ['.mcp.json', 'mcp.json']) {
165
- const path = join(stage, file);
166
- if (!existsSync(path)) continue;
167
- add(file, parseJson(path, file).mcpServers);
168
- }
169
- return found ? merged : undefined;
170
- }
171
- function prepareCommands(stage: string, supplied?: string): string | undefined {
172
- const commands = join(stage, 'commands');
173
- if (existsSync(commands) && !statSync(commands).isDirectory()) throw new Error(`Kimi commands path is not a directory: ${commands}`);
174
- const claude = join(stage, '.claude', 'commands');
175
- const selected = supplied === undefined ? (existsSync(claude) ? './.claude/commands/' : existsSync(commands) ? './commands/' : undefined) : supplied.endsWith('/') ? supplied : `${supplied}/`;
176
- if (selected === undefined) return undefined;
177
- const dir = selected.startsWith('./.claude/') ? claude : commands;
178
- if (!existsSync(dir)) throw new Error(`Kimi native commands pointer has no directory: ${selected}`);
179
- assertMarkdownCommandTree(dir);
180
- if (!containsMarkdown(dir)) throw new Error(`Kimi native commands pointer has no Markdown commands: ${selected}`);
181
- return selected;
182
- }
183
- function assertKimiSkillPolicies(dir: string): void { for (const name of readdirSync(dir)) { const path = join(dir, name); const stat = lstatSync(path); if (stat.isDirectory()) assertKimiSkillPolicies(path); else if (stat.isFile() && name === 'SKILL.md') { const frontmatter = openingFrontmatter(readFileSync(path, 'utf8'), path); if (frontmatter !== undefined && ['user-invocable', 'user_invocable'].some(key => Object.hasOwn(frontmatter, key))) throw new Error(`Kimi does not support user-invocable skill policy: ${path}`); const hyphen = frontmatter?.['disable-model-invocation'], underscore = frontmatter?.disable_model_invocation; if (hyphen !== undefined && underscore !== undefined && hyphen !== underscore) throw new Error(`Kimi disable-model-invocation aliases conflict: ${path}`); for (const value of [hyphen, underscore]) if (value !== undefined && typeof value !== 'boolean') throw new Error(`Kimi disable-model-invocation must be boolean: ${path}`); } } }
184
- function openingFrontmatter(raw: string, path: string): Record<string, unknown> | undefined { const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw); if (match === null) return undefined; let parsed: unknown; try { parsed = Bun.YAML.parse(match[1] ?? ''); } catch { throw new Error(`Kimi skill frontmatter has invalid YAML: ${path}`); } if (!isObject(parsed)) throw new Error(`Kimi skill frontmatter must be an object: ${path}`); return parsed; }
185
- function assertMarkdownCommandTree(dir: string): void { for (const name of readdirSync(dir)) { const path = join(dir, name); const stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`Kimi command source contains symlink: ${path}`); if (stat.isDirectory()) assertMarkdownCommandTree(path); else if (!stat.isFile() || !name.endsWith('.md')) throw new Error(`Kimi command projection cannot preserve non-Markdown resource: ${path}`); else assertKimiCommand(path); } }
186
- function assertKimiCommand(path: string): void { const raw = readFileSync(path, 'utf8'); const frontmatter = openingFrontmatter(raw, path); if (frontmatter === undefined) throw new Error(`Kimi command needs YAML frontmatter: ${path}`); for (const key of Object.keys(frontmatter)) if (!['name', 'description'].includes(key)) throw new Error(`Kimi command metadata is unsupported: ${key} in ${path}`); const body = raw.slice(raw.indexOf('\n---', 4) + 4); if (/!`[\s\S]*?`|@\{|\$\d+(?!\w)/u.test(body)) throw new Error(`Kimi command preprocessing is unsupported: ${path}`); }
187
- function containsMarkdown(dir: string): boolean { return readdirSync(dir).some(name => { const path = join(dir, name); return statSync(path).isDirectory() ? containsMarkdown(path) : name.endsWith('.md'); }); }
188
- function readSourceManifest(stage: string): Record<string, unknown> {
189
- const file = [join(stage, 'plugin.json'), join(stage, '.plugin', 'plugin.json')].find(existsSync);
190
- if (file === undefined) throw new Error('Kimi stage has no Agent Plugins manifest');
191
- return parseJson(file, 'Agent Plugins manifest');
192
- }
193
- function parseJson(path: string, label: string): Record<string, unknown> { try { const raw: unknown = JSON.parse(readFileSync(path, 'utf8')); if (!isObject(raw)) throw new Error('must be an object'); return raw; } catch (error) { throw new Error(`invalid ${label}: ${path} (${(error as Error).message})`); } }
194
- function validateNativeIdentity(target: string, id: string): void { const file = join(target, 'kimi.plugin.json'); if (!existsSync(file) || parseJson(file, 'Kimi native manifest').name !== id) throw new Error(`unowned Kimi managed plugin has no matching native manifest: ${target}`); }
195
- function readOwnership(target: string): Ownership | null { const file = join(target, MARKER); const stat = lstatIfPresent(file); if (stat === undefined) return null; if (stat.isSymbolicLink()) throw new Error(`plgnz ownership marker is a symlink: ${file}`); if (!stat.isFile()) throw new Error(`plgnz ownership marker is not a file: ${file}`); const value = parseJson(file, 'plgnz ownership marker'); if (typeof value.source !== 'string' || typeof value.pluginId !== 'string' || typeof value.fingerprint !== 'string') throw new Error(`invalid plgnz ownership marker: ${file}`); return { source: value.source, pluginId: value.pluginId, fingerprint: value.fingerprint }; }
196
- function sameTree(left: string, right: string): boolean { if (!existsSync(right)) return false; const readBytes = readFileSync as unknown as (path: string) => Uint8Array; const list = (root: string): string[] => { const out: string[] = []; const walk = (dir: string, prefix: string): void => { for (const name of readdirSync(dir).sort()) { if (name === MARKER) continue; const path = join(dir, name); const rel = prefix ? `${prefix}/${name}` : name; const stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`Kimi plugin contains symlink: ${path}`); if (stat.isDirectory()) walk(path, rel); else if (stat.isFile()) out.push(`${rel}:${Array.from(readBytes(path)).join(',')}`); else throw new Error(`Kimi plugin contains unsupported file: ${path}`); } }; walk(root, ''); return out; }; return JSON.stringify(list(left)) === JSON.stringify(list(right)); }
197
- function assertNoSymlinks(root: string): void { const walk = (dir: string): void => { const stat = lstatSync(dir); if (stat.isSymbolicLink()) throw new Error(`Kimi plugin contains symlink: ${dir}`); if (!stat.isDirectory()) throw new Error(`Kimi plugin path is not a directory: ${dir}`); for (const name of readdirSync(dir)) { const path = join(dir, name); const child = lstatSync(path); if (child.isSymbolicLink()) throw new Error(`Kimi plugin contains symlink: ${path}`); if (child.isDirectory()) walk(path); else if (!child.isFile()) throw new Error(`Kimi plugin contains unsupported file: ${path}`); } }; walk(root); }
198
- function moveAside(target: string, root: string): { commit(): void; rollback(): void } { if (!existsSync(target)) return { commit: () => {}, rollback: () => {} }; mkdirSync(root, { recursive: true }); const dir = mkdtempSync(join(root, '.plgnz-kimi-backup-')); const previous = join(dir, 'previous'); renameSync(target, previous); return { commit: () => rmSync(dir, { recursive: true, force: true }), rollback: () => { renameSync(previous, target); rmSync(dir, { recursive: true, force: true }); } }; }
199
- function lstatIfPresent(path: string) { try { return lstatSync(path); } catch (error) { if ((error as { code?: string }).code === 'ENOENT') return undefined; throw error; } }
200
- function assertManagedPath(root: string, target: string): void { const base = resolve(root); const selected = resolve(target); if (selected !== base && !selected.startsWith(`${base}/`)) throw new Error(`Kimi managed path escapes its home: ${target}`); let current = base; for (const part of selected.slice(base.length).split('/').filter(Boolean)) { if (lstatIfPresent(current) === undefined) break; const stat = lstatSync(current); if (stat.isSymbolicLink()) throw new Error(`Kimi managed path component is a symlink: ${current}`); if (!stat.isDirectory()) throw new Error(`Kimi managed path component is not a directory: ${current}`); current = join(current, part); } const stat = lstatIfPresent(current); if (stat !== undefined) { if (stat.isSymbolicLink()) throw new Error(`Kimi managed path component is a symlink: ${current}`); if (current !== selected && !stat.isDirectory()) throw new Error(`Kimi managed path component is not a directory: ${current}`); } }
201
- function assertFilePath(root: string, path: string): void { assertManagedPath(root, dirname(path)); const stat = lstatIfPresent(path); if (stat === undefined) return; if (stat.isSymbolicLink()) throw new Error(`Kimi managed metadata is a symlink: ${path}`); if (!stat.isFile()) throw new Error(`Kimi managed metadata is not a file: ${path}`); }
202
-
203
- function resolveKimiBinary(): string {
204
- const explicit = process.env['OPEN_PLUGIN_KIMI_BIN'];
205
- const binary = explicit ?? join(process.env['HOME'] ?? '.', '.local', 'share', 'kimi-code', 'bin', 'kimi');
206
- if (!existsSync(binary)) throw new Error(`current Kimi Code binary not found: ${binary}`);
207
- const result = Bun.spawnSync([binary, '--version'], { stdout: 'pipe', stderr: 'pipe' });
208
- const version = new TextDecoder().decode(result.stdout).trim();
209
- const match = /^(\d+)\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.exec(version);
210
- if (result.exitCode !== 0 || match === null || Number(match[1]) === 0) throw new Error(`current Kimi Code binary required; legacy or unsupported binary: ${binary}`);
211
- return binary;
212
- }
213
- async function reservePort(): Promise<number> { const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => new Response('') }); const port = server.port; server.stop(true); return port; }
214
- async function nativeInstall(source: string, home: string, binary: string): Promise<void> {
215
- if (!isAbsolute(home) || !isAbsolute(source)) throw new Error('Kimi native source and home must be absolute');
216
- await withKimiServer(home, binary, request => { request('POST', '/api/v1/plugins', { source }); request('POST', `/api/v1/plugins/${encodeURIComponent(source.split('/').at(-1) ?? '')}:enable`); });
217
- }
218
- async function nativeRemove(home: string, binary: string, id: string): Promise<void> {
219
- if (!isAbsolute(home)) throw new Error('Kimi native home must be absolute');
220
- await withKimiServer(home, binary, request => { request('POST', `/api/v1/plugins/${encodeURIComponent(id)}:remove`); });
221
- }
222
- async function withKimiServer(home: string, binary: string, operation: (request: (method: string, path: string, body?: unknown) => Record<string, unknown>) => void): Promise<void> {
223
- const port = await reservePort(); const base = `http://127.0.0.1:${port}`;
224
- const child = Bun.spawn([binary, 'web', '--no-open', '--port', String(port), '--log-level', 'silent'], { stdout: 'pipe', stderr: 'pipe', env: { ...process.env, KIMI_CODE_HOME: home, NO_PROXY: '127.0.0.1,localhost', no_proxy: '127.0.0.1,localhost' } });
225
- let token: string | undefined;
226
- try {
227
- let healthy = false;
228
- for (let attempt = 0; attempt < 90; attempt += 1) { if (child.exitCode !== null) throw new Error(`Kimi web exited before becoming healthy (exit ${child.exitCode})`); try { const value = curlJson(`${base}/api/v1/healthz`); if (value.code === 0 && isObject(value.data) && value.data.ok === true) { healthy = true; break; } } catch {} await Bun.sleep(50); }
229
- if (!healthy) throw new Error('Kimi web server did not become healthy within the lifecycle deadline');
230
- token = readFileSync(join(home, 'server.token'), 'utf8').trim(); if (!token) throw new Error('Kimi server did not create its bearer token');
231
- const request = (method: string, path: string, body?: unknown): Record<string, unknown> => { const value = curlJson(`${base}${path}`, method, token, body); if (value.code !== 0) throw new Error(`Kimi ${method} ${path}: ${String(value.msg ?? value.code)}`); return value; };
232
- operation(request);
233
- } finally {
234
- if (token) { try { curlJson(`${base}/api/v1/shutdown`, 'POST', token); } catch {} }
235
- await Promise.race([child.exited, Bun.sleep(100)]); if (child.exitCode === null) { child.kill(); await Promise.race([child.exited, Bun.sleep(1_000)]); }
236
- }
237
- }
238
- function curlJson(url: string, method = 'GET', token?: string, body?: unknown): Record<string, unknown> { const args = ['--noproxy', '*', '--silent', '--show-error', '--max-time', '2', '--request', method, ...(token === undefined ? [] : ['--header', `Authorization: Bearer ${token}`]), ...(body === undefined ? [] : ['--header', 'Content-Type: application/json', '--data', JSON.stringify(body)]), url]; const result = Bun.spawnSync(['curl', ...args], { stdout: 'pipe', stderr: 'pipe' }); const text = new TextDecoder().decode(result.stdout); if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr).trim() || `curl exit ${result.exitCode}`); try { const value: unknown = JSON.parse(text); if (!isObject(value)) throw new Error('must be an object'); return value; } catch (error) { throw new Error(`Kimi returned invalid JSON: ${(error as Error).message}`); } }