larkin 0.5.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ External pi is accepted at 0.84.2 or newer instead of exactly 0.84.2; 0.5.0 refused newer pi and left every migrated Agent not ready.
6
+
3
7
  ## 0.5.0
4
8
 
5
9
  BREAKING — builtin Pi is removed. Larkin supports only externally installed `pi`, `codex`, and `claude`. If the selected runtime is not installed, setup, runtime switch, and readiness fail with an explicit missing-install message.
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = exports.PINNED_PI_VERSION = void 0;
6
+ exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = exports.MINIMUM_PI_VERSION = void 0;
7
7
  exports.calculatePiCompactionSettings = calculatePiCompactionSettings;
8
8
  exports.isPiNativeCompactionRequired = isPiNativeCompactionRequired;
9
9
  exports.assertEffectivePiCompactionSettings = assertEffectivePiCompactionSettings;
@@ -14,6 +14,7 @@ exports.prepareOwnedPiDirectory = prepareOwnedPiDirectory;
14
14
  exports.mergeOwnedPiSettings = mergeOwnedPiSettings;
15
15
  exports.writeOwnedPiSettings = writeOwnedPiSettings;
16
16
  exports.readOwnedPiSettings = readOwnedPiSettings;
17
+ exports.olderThanMinimumPiVersionMessage = olderThanMinimumPiVersionMessage;
17
18
  exports.parsePiExecutableVersion = parsePiExecutableVersion;
18
19
  exports.verifyPiCapabilities = verifyPiCapabilities;
19
20
  exports.prepareOwnedPiEnvironment = prepareOwnedPiEnvironment;
@@ -24,7 +25,8 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
24
25
  const node_fs_1 = __importDefault(require("node:fs"));
25
26
  const node_os_1 = __importDefault(require("node:os"));
26
27
  const node_path_1 = __importDefault(require("node:path"));
27
- exports.PINNED_PI_VERSION = "0.84.2";
28
+ /** External `pi` must be this version or newer; handshake still guards protocol compatibility. */
29
+ exports.MINIMUM_PI_VERSION = "0.84.2";
28
30
  /** Fallback values used while importing a Pi profile, before Pi reports its model. */
29
31
  exports.PI_CONTEXT_WINDOW = 272_000;
30
32
  exports.COMPACTION_RESERVE_TOKENS = 40_800;
@@ -254,21 +256,51 @@ function readOwnedPiSettings(directory) {
254
256
  }
255
257
  return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
256
258
  }
259
+ const PI_VERSION_LINE = /^(?:pi(?:-coding-agent)?(?:\s+version)?\s+v?)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/i;
260
+ const PI_VERSION_CORE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
261
+ function comparePiNumericVersions(left, right) {
262
+ const leftParts = left.split(".").map(Number);
263
+ const rightParts = right.split(".").map(Number);
264
+ for (let index = 0; index < 3; index += 1) {
265
+ if (leftParts[index] !== rightParts[index])
266
+ return Math.sign(leftParts[index] - rightParts[index]);
267
+ }
268
+ return 0;
269
+ }
270
+ function olderThanMinimumPiVersionMessage(version) {
271
+ return `Pi executable version ${version} is older than the minimum ${exports.MINIMUM_PI_VERSION}`;
272
+ }
273
+ function assertSupportedPiVersion(version) {
274
+ if (typeof version !== "string")
275
+ throw new Error(`Pi executable version ${exports.MINIMUM_PI_VERSION} or newer is required`);
276
+ const raw = version.trim();
277
+ const match = PI_VERSION_CORE.exec(raw);
278
+ if (!match)
279
+ throw new Error(`Pi executable version ${raw || "missing"} is not a supported version`);
280
+ const numeric = `${match[1]}.${match[2]}.${match[3]}`;
281
+ if (match[4]) {
282
+ throw new Error(`Pi executable version ${raw} is unsupported: SemVer ${raw} < ${numeric}; Larkin supports stable external pi only`);
283
+ }
284
+ if (comparePiNumericVersions(numeric, exports.MINIMUM_PI_VERSION) < 0) {
285
+ throw new Error(olderThanMinimumPiVersionMessage(numeric));
286
+ }
287
+ return numeric;
288
+ }
257
289
  function parsePiExecutableVersion(output) {
258
290
  const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
259
291
  if (lines.length !== 1)
260
292
  throw new Error("Pi executable version output must contain exactly one version line");
261
- const line = lines[0];
262
- // Pi's bare version is canonical; permit only the fixed official display prefix.
263
- const escapedVersion = exports.PINNED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
264
- if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
265
- throw new Error(`Pi executable version must be exactly ${exports.PINNED_PI_VERSION}`);
266
- }
267
- return exports.PINNED_PI_VERSION;
293
+ const match = PI_VERSION_LINE.exec(lines[0]);
294
+ if (!match)
295
+ throw new Error("Pi executable version output is not a supported version line");
296
+ const version = `${match[1]}.${match[2]}.${match[3]}`;
297
+ if (comparePiNumericVersions(version, exports.MINIMUM_PI_VERSION) < 0) {
298
+ throw new Error(olderThanMinimumPiVersionMessage(version));
299
+ }
300
+ return version;
268
301
  }
269
302
  function verifyPiCapabilities(capabilities) {
270
- if (capabilities.version !== exports.PINNED_PI_VERSION)
271
- throw new Error(`trusted Pi version ${exports.PINNED_PI_VERSION} is required`);
303
+ assertSupportedPiVersion(capabilities.version);
272
304
  if (capabilities.trustedProtocol === true) {
273
305
  throw new Error("external Pi cannot use a trusted protocol bypass");
274
306
  }
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- export const PINNED_PI_VERSION = "0.84.2";
5
+ /** External `pi` must be this version or newer; handshake still guards protocol compatibility. */
6
+ export const MINIMUM_PI_VERSION = "0.84.2";
6
7
  /** Fallback values used while importing a Pi profile, before Pi reports its model. */
7
8
  export const PI_CONTEXT_WINDOW = 272_000;
8
9
  export const COMPACTION_RESERVE_TOKENS = 40_800;
@@ -232,21 +233,51 @@ export function readOwnedPiSettings(directory) {
232
233
  }
233
234
  return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
234
235
  }
236
+ const PI_VERSION_LINE = /^(?:pi(?:-coding-agent)?(?:\s+version)?\s+v?)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/i;
237
+ const PI_VERSION_CORE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
238
+ function comparePiNumericVersions(left, right) {
239
+ const leftParts = left.split(".").map(Number);
240
+ const rightParts = right.split(".").map(Number);
241
+ for (let index = 0; index < 3; index += 1) {
242
+ if (leftParts[index] !== rightParts[index])
243
+ return Math.sign(leftParts[index] - rightParts[index]);
244
+ }
245
+ return 0;
246
+ }
247
+ export function olderThanMinimumPiVersionMessage(version) {
248
+ return `Pi executable version ${version} is older than the minimum ${MINIMUM_PI_VERSION}`;
249
+ }
250
+ function assertSupportedPiVersion(version) {
251
+ if (typeof version !== "string")
252
+ throw new Error(`Pi executable version ${MINIMUM_PI_VERSION} or newer is required`);
253
+ const raw = version.trim();
254
+ const match = PI_VERSION_CORE.exec(raw);
255
+ if (!match)
256
+ throw new Error(`Pi executable version ${raw || "missing"} is not a supported version`);
257
+ const numeric = `${match[1]}.${match[2]}.${match[3]}`;
258
+ if (match[4]) {
259
+ throw new Error(`Pi executable version ${raw} is unsupported: SemVer ${raw} < ${numeric}; Larkin supports stable external pi only`);
260
+ }
261
+ if (comparePiNumericVersions(numeric, MINIMUM_PI_VERSION) < 0) {
262
+ throw new Error(olderThanMinimumPiVersionMessage(numeric));
263
+ }
264
+ return numeric;
265
+ }
235
266
  export function parsePiExecutableVersion(output) {
236
267
  const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
237
268
  if (lines.length !== 1)
238
269
  throw new Error("Pi executable version output must contain exactly one version line");
239
- const line = lines[0];
240
- // Pi's bare version is canonical; permit only the fixed official display prefix.
241
- const escapedVersion = PINNED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
242
- if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
243
- throw new Error(`Pi executable version must be exactly ${PINNED_PI_VERSION}`);
244
- }
245
- return PINNED_PI_VERSION;
270
+ const match = PI_VERSION_LINE.exec(lines[0]);
271
+ if (!match)
272
+ throw new Error("Pi executable version output is not a supported version line");
273
+ const version = `${match[1]}.${match[2]}.${match[3]}`;
274
+ if (comparePiNumericVersions(version, MINIMUM_PI_VERSION) < 0) {
275
+ throw new Error(olderThanMinimumPiVersionMessage(version));
276
+ }
277
+ return version;
246
278
  }
247
279
  export function verifyPiCapabilities(capabilities) {
248
- if (capabilities.version !== PINNED_PI_VERSION)
249
- throw new Error(`trusted Pi version ${PINNED_PI_VERSION} is required`);
280
+ assertSupportedPiVersion(capabilities.version);
250
281
  if (capabilities.trustedProtocol === true) {
251
282
  throw new Error("external Pi cannot use a trusted protocol bypass");
252
283
  }
@@ -14,7 +14,7 @@ import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-s
14
14
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
15
15
  import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
16
16
  import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
17
- import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, PINNED_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
17
+ import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
18
18
  function piAgentDirectory(configDir, agentId) {
19
19
  return path.join(path.resolve(configDir), "providers", "pi", agentId);
20
20
  }
@@ -1048,7 +1048,7 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1048
1048
  ? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
1049
1049
  cwd: input.workspaceDir, env: childEnv, encoding: "utf8", timeout: 5_000,
1050
1050
  }).stdout || ""))
1051
- : PINNED_PI_VERSION;
1051
+ : MINIMUM_PI_VERSION;
1052
1052
  const extensionInput = { piCommand: command, env: childEnv, platform: process.platform };
1053
1053
  // 假 spawn 不需要真实 extension 路径;默认 resolver 会 spawnSync(`pi --version`)。
1054
1054
  const extensionArgs = dependencies.resolvePiProcessExtensionArgs
@@ -53,6 +53,7 @@ exports.probeNativeRuntimeReadiness = probeNativeRuntimeReadiness;
53
53
  const node_fs_1 = __importDefault(require("node:fs"));
54
54
  const node_path_1 = __importDefault(require("node:path"));
55
55
  const node_child_process_1 = require("node:child_process");
56
+ const pi_compaction_recovery_js_1 = require("./pi-compaction-recovery.cjs");
56
57
  function safeProviderId(value) {
57
58
  const provider = typeof value === "string" ? value.trim() : "";
58
59
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
@@ -191,6 +192,14 @@ function classifyRuntimePrerequisite(runtime, error, executable) {
191
192
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
192
193
  nextAction: runtimeLoginNextAction(runtime),
193
194
  };
195
+ if (/older than the minimum|not a supported version(?: line)?|version output must contain exactly one/i.test(reason)) {
196
+ return {
197
+ runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
198
+ nextAction: runtime === "pi"
199
+ ? `Upgrade pi to ${pi_compaction_recovery_js_1.MINIMUM_PI_VERSION} or newer`
200
+ : `Upgrade ${runtime}, then retry.`,
201
+ };
202
+ }
194
203
  if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason)) {
195
204
  return {
196
205
  runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
@@ -238,7 +247,19 @@ async function probeNativeRuntimeReadiness(options) {
238
247
  nextAction: runtimeInstallNextAction(options.runtime),
239
248
  };
240
249
  }
241
- const version = executableVersion(executable, env, options.commandArgs);
250
+ let version = executableVersion(executable, env, options.commandArgs);
251
+ if (options.runtime === "pi") {
252
+ try {
253
+ const probe = (0, node_child_process_1.spawnSync)(executable, [...options.commandArgs ?? [], "--version"], {
254
+ env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024,
255
+ });
256
+ version = (0, pi_compaction_recovery_js_1.parsePiExecutableVersion)(String(probe.stdout || ""));
257
+ }
258
+ catch (error) {
259
+ const classified = classifyRuntimePrerequisite("pi", error, executable);
260
+ return { ...classified, executable, ...(version ? { version } : {}) };
261
+ }
262
+ }
242
263
  try {
243
264
  if (options.runtime === "pi") {
244
265
  const { discoverPiModelCatalog } = await Promise.resolve().then(() => __importStar(require("./pi-model-catalog.cjs")));
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
+ import { MINIMUM_PI_VERSION, parsePiExecutableVersion } from "./pi-compaction-recovery.mjs";
4
5
  export function safeProviderId(value) {
5
6
  const provider = typeof value === "string" ? value.trim() : "";
6
7
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
@@ -138,6 +139,14 @@ export function classifyRuntimePrerequisite(runtime, error, executable) {
138
139
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
139
140
  nextAction: runtimeLoginNextAction(runtime),
140
141
  };
142
+ if (/older than the minimum|not a supported version(?: line)?|version output must contain exactly one/i.test(reason)) {
143
+ return {
144
+ runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
145
+ nextAction: runtime === "pi"
146
+ ? `Upgrade pi to ${MINIMUM_PI_VERSION} or newer`
147
+ : `Upgrade ${runtime}, then retry.`,
148
+ };
149
+ }
141
150
  if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason)) {
142
151
  return {
143
152
  runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
@@ -185,7 +194,19 @@ export async function probeNativeRuntimeReadiness(options) {
185
194
  nextAction: runtimeInstallNextAction(options.runtime),
186
195
  };
187
196
  }
188
- const version = executableVersion(executable, env, options.commandArgs);
197
+ let version = executableVersion(executable, env, options.commandArgs);
198
+ if (options.runtime === "pi") {
199
+ try {
200
+ const probe = spawnSync(executable, [...options.commandArgs ?? [], "--version"], {
201
+ env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024,
202
+ });
203
+ version = parsePiExecutableVersion(String(probe.stdout || ""));
204
+ }
205
+ catch (error) {
206
+ const classified = classifyRuntimePrerequisite("pi", error, executable);
207
+ return { ...classified, executable, ...(version ? { version } : {}) };
208
+ }
209
+ }
189
210
  try {
190
211
  if (options.runtime === "pi") {
191
212
  const { discoverPiModelCatalog } = await import("./pi-model-catalog.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [