kimi-plugin-cc 2.4.5 → 2.5.2

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 (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +103 -29
  5. package/dist/commands/setup.js +78 -51
  6. package/dist/dsh/companion.js +177 -0
  7. package/dist/dsh/index.js +169 -0
  8. package/dist/hooks/install-paths.js +2 -2
  9. package/dist/hooks/managed-block.js +86 -20
  10. package/dist/version.js +1 -1
  11. package/docs/migration.md +4 -2
  12. package/docs/models.md +5 -1
  13. package/docs/registry-releases.md +84 -20
  14. package/docs/safety.md +25 -10
  15. package/docs/upstream-2.4.1-certification.md +6 -0
  16. package/package.json +75 -8
  17. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  18. package/plugins/kimi-codex/dist/commands/setup.js +78 -51
  19. package/plugins/kimi-codex/dist/dsh/companion.js +177 -0
  20. package/plugins/kimi-codex/dist/dsh/index.js +169 -0
  21. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  22. package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
  23. package/plugins/kimi-codex/dist/version.js +1 -1
  24. package/plugins/kimi-dsh/LICENSE +201 -0
  25. package/plugins/kimi-dsh/README.md +83 -0
  26. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  27. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  28. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  29. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  30. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  31. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  32. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  33. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  34. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  35. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  36. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  37. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  38. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  39. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  40. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  41. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  42. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  43. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  44. package/plugins/kimi-dsh/dist/commands/setup.js +1074 -0
  45. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  46. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  47. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  48. package/plugins/kimi-dsh/dist/companion.js +97 -0
  49. package/plugins/kimi-dsh/dist/config.js +22 -0
  50. package/plugins/kimi-dsh/dist/dsh/companion.js +177 -0
  51. package/plugins/kimi-dsh/dist/dsh/index.js +169 -0
  52. package/plugins/kimi-dsh/dist/errors.js +40 -0
  53. package/plugins/kimi-dsh/dist/git.js +224 -0
  54. package/plugins/kimi-dsh/dist/help.js +43 -0
  55. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  56. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  57. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  58. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  59. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  60. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  61. package/plugins/kimi-dsh/dist/hooks/managed-block.js +685 -0
  62. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  63. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  64. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  65. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  66. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  67. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  68. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  69. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  70. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  71. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  72. package/plugins/kimi-dsh/dist/logging.js +22 -0
  73. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  74. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  75. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  76. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  77. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  78. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  79. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  80. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  81. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  82. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  83. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  84. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  85. package/plugins/kimi-dsh/dist/package.json +1 -0
  86. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  87. package/plugins/kimi-dsh/dist/paths.js +27 -0
  88. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  89. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  90. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  91. package/plugins/kimi-dsh/dist/render.js +177 -0
  92. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  93. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  94. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  95. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  96. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  97. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  98. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  99. package/plugins/kimi-dsh/dist/types.js +1 -0
  100. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  101. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  102. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  103. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  104. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  105. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  106. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  107. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  111. package/plugins/kimi-dsh/dist/version.js +7 -0
  112. package/plugins/kimi-dsh/package.json +78 -0
  113. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  114. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,98 @@
1
+ import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { RuntimeError } from "./errors.js";
5
+ // Use the loaded module, not the shared PLUGIN_ROOT/CLAUDE_PLUGIN_ROOT variables.
6
+ // Both source and compiled modules live one directory below the package root.
7
+ const loadedPackageRoot = fileURLToPath(new URL("../", import.meta.url));
8
+ /** Resolve state without silently trusting a different plugin's shared env. */
9
+ export function resolvePluginDataRoot(env, packageRoot = loadedPackageRoot) {
10
+ const explicit = env.KIMI_PLUGIN_CC_DATA;
11
+ if (explicit !== undefined) {
12
+ if (!path.isAbsolute(explicit)) {
13
+ throw new RuntimeError("INVALID_PLUGIN_DATA", "KIMI_PLUGIN_CC_DATA must be an absolute directory path, not an empty or relative value.", "paths");
14
+ }
15
+ return path.normalize(explicit);
16
+ }
17
+ const expected = installedDataRoot(packageRoot);
18
+ const shared = [env.CLAUDE_PLUGIN_DATA, env.PLUGIN_DATA].filter((value) => Boolean(value));
19
+ if (expected) {
20
+ if (shared.some((value) => !path.isAbsolute(value) || !sameDataRoot(value, expected))) {
21
+ throw new RuntimeError("PLUGIN_DATA_CONFLICT", "Shared plugin data variables do not match this installation. Remove the conflicting variables or set KIMI_PLUGIN_CC_DATA to the intended Kimi data directory. No data was moved.", "paths", { details: { expected_data_root: expected, override_variable: "KIMI_PLUGIN_CC_DATA" } });
22
+ }
23
+ return expected;
24
+ }
25
+ // Checkout/custom launch compatibility: we cannot infer ownership from an
26
+ // arbitrary directory or from a marker that may not exist on first setup.
27
+ // Preserve the old location when unambiguous; require an explicit choice otherwise.
28
+ if (shared.length === 2 && !sameDataRoot(shared[0], shared[1])) {
29
+ throw new RuntimeError("PLUGIN_DATA_CONFLICT", "CLAUDE_PLUGIN_DATA and PLUGIN_DATA disagree. Set KIMI_PLUGIN_CC_DATA to the intended Kimi data directory before retrying. No data was moved.", "paths");
30
+ }
31
+ if (shared[0])
32
+ return shared[0];
33
+ if (env.KIMI_PLUGIN_CC_SHELL_LAUNCH === "1" && (env.CODEX_HOME || env.HOME)) {
34
+ const dataRoot = env.CODEX_HOME
35
+ ? path.join(env.CODEX_HOME, "plugins", "data")
36
+ : path.join(env.HOME, ".codex", "plugins", "data");
37
+ return path.join(dataRoot, "kimi-marketplace-kimi");
38
+ }
39
+ throw new RuntimeError("MISSING_PLUGIN_DATA", "Set KIMI_PLUGIN_CC_DATA to an absolute writable data directory. Custom launches may also use an unambiguous CLAUDE_PLUGIN_DATA or PLUGIN_DATA value.", "paths");
40
+ }
41
+ function installedDataRoot(packageRoot) {
42
+ const root = path.resolve(packageRoot);
43
+ const pluginDir = path.dirname(root);
44
+ const marketplaceDir = path.dirname(pluginDir);
45
+ const cacheDir = path.dirname(marketplaceDir);
46
+ const pluginsDir = path.dirname(cacheDir);
47
+ if (path.basename(pluginDir) !== "kimi" || path.basename(cacheDir) !== "cache" || path.basename(pluginsDir) !== "plugins") {
48
+ return undefined;
49
+ }
50
+ const marketplace = path.basename(marketplaceDir);
51
+ if (existsSync(path.join(root, ".codex-plugin", "plugin.json"))) {
52
+ return path.join(pluginsDir, "data", `${marketplace}-kimi`);
53
+ }
54
+ if (existsSync(path.join(root, ".claude-plugin", "plugin.json"))) {
55
+ // Claude documents plugin-id sanitization for persistent data directories.
56
+ const id = `kimi@${marketplace}`.replace(/[^a-zA-Z0-9_-]/g, "-");
57
+ return path.join(pluginsDir, "data", id);
58
+ }
59
+ return undefined;
60
+ }
61
+ function sameDataRoot(left, right) {
62
+ if (path.resolve(left) === path.resolve(right))
63
+ return true;
64
+ const canonicalLeft = canonicalDataRoot(left);
65
+ return canonicalLeft !== undefined && canonicalLeft === canonicalDataRoot(right);
66
+ }
67
+ /** Resolve existing ancestors without creating a first-run data directory. */
68
+ function canonicalDataRoot(value) {
69
+ let cursor = path.resolve(value);
70
+ const missing = [];
71
+ for (;;) {
72
+ try {
73
+ const resolved = realpathSync(cursor);
74
+ if (missing.length > 0 && !statSync(cursor).isDirectory())
75
+ return undefined;
76
+ return path.join(resolved, ...missing);
77
+ }
78
+ catch (error) {
79
+ if (error.code !== "ENOENT")
80
+ return undefined;
81
+ // ENOENT can also mean a dangling symlink. Do not treat an existing
82
+ // link as a missing directory and accidentally bypass its destination.
83
+ try {
84
+ lstatSync(cursor);
85
+ return undefined;
86
+ }
87
+ catch (statError) {
88
+ if (statError.code !== "ENOENT")
89
+ return undefined;
90
+ }
91
+ const parent = path.dirname(cursor);
92
+ if (parent === cursor)
93
+ return undefined;
94
+ missing.unshift(path.basename(cursor));
95
+ cursor = parent;
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,479 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ const MAX_DESCENDANT_DEPTH = 8;
6
+ const MAX_DESCENDANT_PIDS = 512;
7
+ const PROCESS_EXIT_POLL_MS = 10;
8
+ const PROCESS_SNAPSHOT_TIMEOUT_MS = 1_000;
9
+ const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 1_000;
10
+ const PROBE_TIMEOUT = Symbol("process-probe-timeout");
11
+ /**
12
+ * Capture descendant ancestry and reusable identities in the same bounded
13
+ * process-table snapshot. There is no PID-only handoff: on Linux, PPID, PGID,
14
+ * state, and start time come from one /proc/<pid>/stat read per row; other
15
+ * POSIX systems use one ps invocation for the whole table.
16
+ */
17
+ export async function collectDescendantIdentities(rootPid) {
18
+ if (!Number.isInteger(rootPid) || rootPid <= 0 || process.platform === "win32")
19
+ return [];
20
+ const snapshot = await raceValueWithTimeout(collectVerifiedDescendantIdentities(rootPid), PROCESS_SNAPSHOT_TIMEOUT_MS);
21
+ if (snapshot === PROBE_TIMEOUT) {
22
+ throw new Error(`process snapshot exceeded ${PROCESS_SNAPSHOT_TIMEOUT_MS}ms`);
23
+ }
24
+ return snapshot;
25
+ }
26
+ /** Return current members of the selected process groups, or undefined when unreadable. */
27
+ export async function collectProcessGroupsIdentities(processGroupIds) {
28
+ if (process.platform === "win32")
29
+ return undefined;
30
+ const groups = new Set(processGroupIds.filter((id) => Number.isInteger(id) && id > 0));
31
+ if (groups.size === 0)
32
+ return [];
33
+ const rows = await raceValueWithTimeout(collectProcessSnapshotRows(), PROCESS_SNAPSHOT_TIMEOUT_MS);
34
+ if (rows === PROBE_TIMEOUT || rows === undefined)
35
+ return undefined;
36
+ return rows.filter((row) => groups.has(row.processGroupId)).map(toProcessIdentity);
37
+ }
38
+ /**
39
+ * Compatibility surface for callers that only need numeric process listings.
40
+ * Cancellation must use collectDescendantIdentities so ancestry and identity
41
+ * cannot be separated by a PID-reuse window.
42
+ */
43
+ export async function collectDescendants(rootPid) {
44
+ try {
45
+ return (await collectDescendantIdentities(rootPid)).map((identity) => identity.pid);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ /** Distinguish a missing process from a failed or timed-out identity probe. */
52
+ export async function probeProcessIdentity(pid) {
53
+ if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
54
+ return { complete: true };
55
+ const read = process.platform === "linux"
56
+ ? readProcessIdentityBatchFromProc([pid])
57
+ : readProcessIdentityBatchWithPs([pid]);
58
+ const batch = await raceValueWithTimeout(read, PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
59
+ if (batch === PROBE_TIMEOUT)
60
+ return { complete: false };
61
+ return { identity: batch.identities.get(pid), complete: batch.complete };
62
+ }
63
+ /** Read one reusable process identity. PID alone is not stable across teardown. */
64
+ export async function readProcessIdentity(pid) {
65
+ return (await probeProcessIdentity(pid)).identity;
66
+ }
67
+ /**
68
+ * Revalidate all captured identities concurrently under one deadline. The
69
+ * default path batches ps fallback reads; an injected per-pid reader is fanned
70
+ * out with Promise.all rather than multiplied by descendant count.
71
+ */
72
+ export async function revalidateProcessIdentities(expectedIdentities, options = {}) {
73
+ const expected = dedupeIdentities(expectedIdentities);
74
+ if (expected.length === 0)
75
+ return { identities: [], complete: true };
76
+ const timeoutMs = Math.max(0, options.timeoutMs ?? PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
77
+ const read = options.identityReader === undefined
78
+ ? readCurrentIdentityBatch(expected)
79
+ : readIdentityBatchWithReader(expected, options.identityReader);
80
+ const batch = await raceValueWithTimeout(read, timeoutMs);
81
+ if (batch === PROBE_TIMEOUT)
82
+ return { identities: [], complete: false };
83
+ const identities = [];
84
+ const groupChanged = [];
85
+ for (const expectedIdentity of expected) {
86
+ const current = batch.identities.get(expectedIdentity.pid);
87
+ if (current === undefined ||
88
+ current.startToken !== expectedIdentity.startToken ||
89
+ isQuiescentState(current.state)) {
90
+ continue;
91
+ }
92
+ if (current.processGroupId !== expectedIdentity.processGroupId) {
93
+ groupChanged.push(current);
94
+ }
95
+ else {
96
+ identities.push(current);
97
+ }
98
+ }
99
+ return {
100
+ identities,
101
+ complete: batch.complete,
102
+ ...(groupChanged.length > 0 ? { groupChanged } : {}),
103
+ };
104
+ }
105
+ /** Return the current identity only when pid, start token, and PGID still match. */
106
+ export async function revalidateProcessIdentity(expected, identityReader) {
107
+ const validation = await revalidateProcessIdentities([expected], { identityReader });
108
+ return validation.identities[0];
109
+ }
110
+ /**
111
+ * Wait until the direct child and every captured identity are quiescent.
112
+ * Every probe is bounded even when the overall wait is infinite, and an
113
+ * incomplete probe is conservatively treated as still active.
114
+ */
115
+ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}) {
116
+ const targets = dedupeIdentities(identities);
117
+ const finiteTimeout = Number.isFinite(timeoutMs);
118
+ const deadline = finiteTimeout ? Date.now() + Math.max(0, timeoutMs) : Number.POSITIVE_INFINITY;
119
+ while (true) {
120
+ const rootAlive = options.isRootAlive?.() === true;
121
+ if (!rootAlive && targets.length === 0)
122
+ return true;
123
+ const remainingMs = deadline - Date.now();
124
+ if (finiteTimeout && remainingMs <= 0)
125
+ return false;
126
+ let active = rootAlive;
127
+ if (!active) {
128
+ const probeTimeoutMs = finiteTimeout
129
+ ? Math.min(PROCESS_IDENTITY_PROBE_TIMEOUT_MS, remainingMs)
130
+ : PROCESS_IDENTITY_PROBE_TIMEOUT_MS;
131
+ const validation = await revalidateProcessIdentities(targets, {
132
+ identityReader: options.identityReader,
133
+ timeoutMs: probeTimeoutMs,
134
+ });
135
+ active = !validation.complete || validation.identities.length > 0 || (validation.groupChanged?.length ?? 0) > 0;
136
+ }
137
+ if (!active)
138
+ return true;
139
+ const afterProbeRemainingMs = deadline - Date.now();
140
+ if (finiteTimeout && afterProbeRemainingMs <= 0)
141
+ return false;
142
+ await sleep(Math.min(PROCESS_EXIT_POLL_MS, afterProbeRemainingMs));
143
+ }
144
+ }
145
+ async function collectVerifiedDescendantIdentities(rootPid) {
146
+ const initialRows = await collectProcessSnapshotRows();
147
+ if (initialRows === undefined)
148
+ throw new Error("process snapshot unavailable");
149
+ const candidateRows = descendantRowsFromSnapshot(rootPid, initialRows);
150
+ const initialRoot = initialRows.find((row) => row.pid === rootPid);
151
+ if (initialRoot === undefined || candidateRows.length === 0)
152
+ return [];
153
+ // A second, targeted snapshot closes the cross-row /proc race: each child
154
+ // must retain its PID and start token. Reparenting and PGID changes are not exit.
155
+ const expectedRows = [initialRoot, ...candidateRows];
156
+ const expectedByPid = new Map(expectedRows.map((row) => [row.pid, row]));
157
+ const verificationRows = await readCurrentSnapshotRows(expectedRows);
158
+ const stableRows = verificationRows.filter((current) => {
159
+ const expected = expectedByPid.get(current.pid);
160
+ return (expected !== undefined &&
161
+ current.startToken === expected.startToken &&
162
+ !isQuiescentState(current.state));
163
+ });
164
+ return stableRows.map(toProcessIdentity);
165
+ }
166
+ function descendantRowsFromSnapshot(rootPid, rows) {
167
+ const root = rows.find((row) => row.pid === rootPid);
168
+ if (root === undefined || isQuiescentState(root.state))
169
+ return [];
170
+ const childrenByParent = new Map();
171
+ for (const row of rows) {
172
+ if (row.pid <= 0 || row.parentPid <= 0 || row.pid === row.parentPid)
173
+ continue;
174
+ const children = childrenByParent.get(row.parentPid);
175
+ if (children === undefined)
176
+ childrenByParent.set(row.parentPid, [row]);
177
+ else
178
+ children.push(row);
179
+ }
180
+ const descendants = [];
181
+ const seen = new Set([rootPid]);
182
+ let frontier = [rootPid];
183
+ for (let depth = 0; depth < MAX_DESCENDANT_DEPTH && frontier.length > 0; depth += 1) {
184
+ const next = [];
185
+ for (const parentPid of frontier) {
186
+ for (const child of childrenByParent.get(parentPid) ?? []) {
187
+ if (seen.has(child.pid))
188
+ continue;
189
+ seen.add(child.pid);
190
+ descendants.push(child);
191
+ if (descendants.length >= MAX_DESCENDANT_PIDS)
192
+ return descendants;
193
+ next.push(child.pid);
194
+ }
195
+ }
196
+ frontier = next;
197
+ }
198
+ return descendants;
199
+ }
200
+ async function readCurrentSnapshotRows(expectedRows) {
201
+ const procPids = [];
202
+ const psPids = [];
203
+ for (const row of expectedRows) {
204
+ if (process.platform === "linux" && row.startToken.startsWith("proc:")) {
205
+ procPids.push(row.pid);
206
+ }
207
+ else {
208
+ psPids.push(row.pid);
209
+ }
210
+ }
211
+ const [procOutcomes, psRows] = await Promise.all([
212
+ Promise.all(procPids.map((pid) => readProcRow(pid))),
213
+ collectProcessSnapshotRowsFromPs(psPids),
214
+ ]);
215
+ if (procOutcomes.some((outcome) => outcome.status === "error") || (psPids.length > 0 && psRows === undefined)) {
216
+ throw new Error("process identity verification failed");
217
+ }
218
+ return [
219
+ ...procOutcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []),
220
+ ...(psRows ?? []),
221
+ ];
222
+ }
223
+ async function collectProcessSnapshotRows() {
224
+ if (process.platform === "linux") {
225
+ const procRows = await collectProcessSnapshotRowsFromProc();
226
+ if (procRows !== undefined)
227
+ return procRows;
228
+ }
229
+ return collectProcessSnapshotRowsFromPs();
230
+ }
231
+ async function collectProcessSnapshotRowsFromProc() {
232
+ let entries;
233
+ try {
234
+ entries = await readdir("/proc");
235
+ }
236
+ catch (err) {
237
+ if (isErrnoException(err, "ENOENT") || isErrnoException(err, "EPERM"))
238
+ return undefined;
239
+ return undefined;
240
+ }
241
+ const outcomes = await Promise.all(entries
242
+ .filter((entry) => /^\d+$/.test(entry))
243
+ .map((entry) => readProcRow(Number.parseInt(entry, 10))));
244
+ if (outcomes.some((outcome) => outcome.status === "error"))
245
+ return undefined;
246
+ return outcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []);
247
+ }
248
+ async function collectProcessSnapshotRowsFromPs(pids) {
249
+ const uniquePids = pids === undefined ? undefined : uniqueValidPids(pids);
250
+ if (uniquePids !== undefined && uniquePids.length === 0)
251
+ return [];
252
+ let stdout;
253
+ try {
254
+ const result = await execFileAsync("ps", uniquePids === undefined
255
+ ? ["-axo", "pid=,ppid=,pgid=,state=,lstart="]
256
+ : ["-o", "pid=,ppid=,pgid=,state=,lstart=", "-p", uniquePids.join(",")], {
257
+ timeout: uniquePids === undefined
258
+ ? PROCESS_SNAPSHOT_TIMEOUT_MS
259
+ : PROCESS_IDENTITY_PROBE_TIMEOUT_MS,
260
+ maxBuffer: uniquePids === undefined ? 2 * 1024 * 1024 : 256 * 1024,
261
+ });
262
+ stdout = result.stdout;
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ return parsePsSnapshotRows(stdout);
268
+ }
269
+ async function readCurrentIdentityBatch(expected) {
270
+ if (process.platform !== "linux") {
271
+ return readProcessIdentityBatchWithPs(expected.map((identity) => identity.pid));
272
+ }
273
+ const procPids = [];
274
+ const psPids = [];
275
+ for (const identity of expected) {
276
+ if (identity.startToken.startsWith("proc:"))
277
+ procPids.push(identity.pid);
278
+ else
279
+ psPids.push(identity.pid);
280
+ }
281
+ const [procBatch, psBatch] = await Promise.all([
282
+ readProcessIdentityBatchFromProc(procPids),
283
+ readProcessIdentityBatchWithPs(psPids),
284
+ ]);
285
+ return mergeIdentityBatches(procBatch, psBatch);
286
+ }
287
+ async function readProcessIdentityBatchFromProc(pids) {
288
+ if (pids.length === 0)
289
+ return { identities: new Map(), complete: true };
290
+ const outcomes = await Promise.all(pids.map((pid) => readProcRow(pid)));
291
+ const identities = new Map();
292
+ let complete = true;
293
+ for (const outcome of outcomes) {
294
+ if (outcome.status === "found") {
295
+ identities.set(outcome.row.pid, toProcessIdentity(outcome.row));
296
+ }
297
+ else if (outcome.status === "error") {
298
+ complete = false;
299
+ }
300
+ }
301
+ return { identities, complete };
302
+ }
303
+ async function readProcessIdentityBatchWithPs(pids) {
304
+ const uniquePids = uniqueValidPids(pids);
305
+ if (uniquePids.length === 0)
306
+ return { identities: new Map(), complete: true };
307
+ let stdout;
308
+ try {
309
+ const result = await execFileAsync("ps", ["-o", "pid=,pgid=,state=,lstart=", "-p", uniquePids.join(",")], { timeout: PROCESS_IDENTITY_PROBE_TIMEOUT_MS, maxBuffer: 256 * 1024 });
310
+ stdout = result.stdout;
311
+ }
312
+ catch (err) {
313
+ if (isExitCode(err, 1))
314
+ return { identities: new Map(), complete: true };
315
+ return { identities: new Map(), complete: false };
316
+ }
317
+ const identities = new Map();
318
+ for (const line of stdout.split("\n")) {
319
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
320
+ if (match === null)
321
+ continue;
322
+ const pid = Number.parseInt(match[1], 10);
323
+ const processGroupId = Number.parseInt(match[2], 10);
324
+ if (!Number.isInteger(pid) || !Number.isInteger(processGroupId) || processGroupId <= 0) {
325
+ continue;
326
+ }
327
+ identities.set(pid, {
328
+ pid,
329
+ processGroupId,
330
+ state: match[3],
331
+ startToken: psStartToken(match[4]),
332
+ });
333
+ }
334
+ return { identities, complete: true };
335
+ }
336
+ async function readIdentityBatchWithReader(expected, identityReader) {
337
+ const outcomes = await Promise.all(expected.map(async (identity) => {
338
+ try {
339
+ return { identity: await identityReader(identity.pid), failed: false };
340
+ }
341
+ catch {
342
+ return { identity: undefined, failed: true };
343
+ }
344
+ }));
345
+ const identities = new Map();
346
+ let complete = true;
347
+ for (const outcome of outcomes) {
348
+ if (outcome.failed)
349
+ complete = false;
350
+ if (outcome.identity !== undefined)
351
+ identities.set(outcome.identity.pid, outcome.identity);
352
+ }
353
+ return { identities, complete };
354
+ }
355
+ async function readProcRow(pid) {
356
+ let stat;
357
+ try {
358
+ stat = await readFile(`/proc/${pid}/stat`, "utf8");
359
+ }
360
+ catch (err) {
361
+ if (isErrnoException(err, "ENOENT") || isErrnoException(err, "ESRCH")) {
362
+ return { status: "missing" };
363
+ }
364
+ return { status: "error" };
365
+ }
366
+ const closeParen = stat.lastIndexOf(")");
367
+ const openParen = stat.indexOf("(");
368
+ if (openParen < 1 || closeParen < openParen)
369
+ return { status: "error" };
370
+ const parsedPid = Number.parseInt(stat.slice(0, openParen).trim(), 10);
371
+ const fields = stat.slice(closeParen + 1).trim().split(/\s+/);
372
+ const state = fields[0];
373
+ const parentPid = Number.parseInt(fields[1] ?? "", 10);
374
+ const processGroupId = Number.parseInt(fields[2] ?? "", 10);
375
+ const startTimeTicks = fields[19];
376
+ if (parsedPid !== pid ||
377
+ state === undefined ||
378
+ startTimeTicks === undefined ||
379
+ !Number.isInteger(parentPid) ||
380
+ !Number.isInteger(processGroupId) ||
381
+ processGroupId <= 0) {
382
+ return { status: "error" };
383
+ }
384
+ return {
385
+ status: "found",
386
+ row: {
387
+ pid,
388
+ parentPid,
389
+ processGroupId,
390
+ startToken: `proc:${startTimeTicks}`,
391
+ state,
392
+ },
393
+ };
394
+ }
395
+ function parsePsSnapshotRows(stdout) {
396
+ const rows = [];
397
+ for (const line of stdout.split("\n")) {
398
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
399
+ if (match === null)
400
+ continue;
401
+ const pid = Number.parseInt(match[1], 10);
402
+ const parentPid = Number.parseInt(match[2], 10);
403
+ const processGroupId = Number.parseInt(match[3], 10);
404
+ if (!Number.isInteger(pid) ||
405
+ !Number.isInteger(parentPid) ||
406
+ !Number.isInteger(processGroupId) ||
407
+ processGroupId <= 0) {
408
+ continue;
409
+ }
410
+ rows.push({
411
+ pid,
412
+ parentPid,
413
+ processGroupId,
414
+ state: match[4],
415
+ startToken: psStartToken(match[5]),
416
+ });
417
+ }
418
+ return rows;
419
+ }
420
+ function mergeIdentityBatches(first, second) {
421
+ return {
422
+ identities: new Map([...first.identities, ...second.identities]),
423
+ complete: first.complete && second.complete,
424
+ };
425
+ }
426
+ function toProcessIdentity(row) {
427
+ return {
428
+ pid: row.pid,
429
+ processGroupId: row.processGroupId,
430
+ startToken: row.startToken,
431
+ state: row.state,
432
+ };
433
+ }
434
+ function dedupeIdentities(identities) {
435
+ const seen = new Set();
436
+ const deduped = [];
437
+ for (const identity of identities) {
438
+ const key = `${identity.pid}:${identity.startToken}:${identity.processGroupId}`;
439
+ if (seen.has(key))
440
+ continue;
441
+ seen.add(key);
442
+ deduped.push(identity);
443
+ }
444
+ return deduped;
445
+ }
446
+ function uniqueValidPids(pids) {
447
+ return [...new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0))];
448
+ }
449
+ function psStartToken(value) {
450
+ return `ps:${value.replace(/\s+/g, " ").trim()}`;
451
+ }
452
+ function isQuiescentState(state) {
453
+ const normalized = state.trim().toUpperCase();
454
+ return normalized.startsWith("Z") || normalized.startsWith("X");
455
+ }
456
+ function sleep(ms) {
457
+ return new Promise((resolve) => setTimeout(resolve, ms));
458
+ }
459
+ async function raceValueWithTimeout(promise, timeoutMs) {
460
+ let timer;
461
+ try {
462
+ return await Promise.race([
463
+ promise,
464
+ new Promise((resolve) => {
465
+ timer = setTimeout(() => resolve(PROBE_TIMEOUT), Math.max(0, timeoutMs));
466
+ }),
467
+ ]);
468
+ }
469
+ finally {
470
+ if (timer !== undefined)
471
+ clearTimeout(timer);
472
+ }
473
+ }
474
+ function isErrnoException(err, code) {
475
+ return typeof err === "object" && err !== null && "code" in err && err.code === code;
476
+ }
477
+ function isExitCode(err, code) {
478
+ return typeof err === "object" && err !== null && "code" in err && err.code === code;
479
+ }
@@ -0,0 +1,18 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ // Node/libuv's default CreateProcess argument quoting (UTF-16 code units).
3
+ export function windowsCommandLineLength(command, args) {
4
+ const quotedLength = (value) => {
5
+ if (value.length > 0 && !/[\s"]/u.test(value))
6
+ return value.length;
7
+ return 2 + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1').length;
8
+ };
9
+ return [command, ...args].reduce((size, value) => size + quotedLength(value) + 1, 0);
10
+ }
11
+ export function assertPromptFitsCommandLine(command, args, platform = process.platform) {
12
+ if (platform !== "win32")
13
+ return;
14
+ const size = windowsCommandLineLength(command, args);
15
+ if (size <= 32_767)
16
+ return;
17
+ throw new RuntimeError("CLI_COMMAND_LINE_TOO_LONG", "The Kimi prompt exceeds the Windows command-line limit; the CLI was not started. Narrow the review or split it into smaller scopes. A short ask prompt can reference a saved diff inside the workspace. Reinstalling Kimi or running setup will not fix this. The current print transport does not support stdin prompts.", "cli-client.pre-spawn", { details: { command_line_utf16_units: size, limit_utf16_units: 32_767, platform } });
18
+ }