@kinlab/kin 0.2.13 → 0.2.14

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/README.md CHANGED
@@ -6,12 +6,14 @@ semantic system of record for software work.
6
6
  ```sh
7
7
  npm install -g @kinlab/kin
8
8
  kin --version
9
+ kin setup --intent agent
9
10
  ```
10
11
 
11
12
  or zero-install:
12
13
 
13
14
  ```sh
14
15
  npx -y @kinlab/kin --version
16
+ npx -y @kinlab/kin setup --intent agent --no-interactive
15
17
  ```
16
18
 
17
19
  ## What it does
@@ -29,6 +31,26 @@ The launcher and the shell installer (`scripts/install.sh`) share the same insta
29
31
  contract (`$KIN_HOME`, default `~/.kin`): either lane satisfies the other, and neither
30
32
  silently downgrades an install the other made.
31
33
 
34
+ Run `kin setup --intent agent` after provisioning. Setup writes the Kin MCP server into
35
+ detected AI clients with the `agent-default` tool profile, adds the managed bin directory
36
+ to your shell profile, installs the shell/session hook, and records the install ledger used
37
+ by `kin setup status`, `kin doctor --fix`, and `kin setup uninstall`.
38
+
39
+ ## Version pinning
40
+
41
+ Every `@kinlab/kin` release pins one managed `kin` release — its own package version.
42
+ Each run compares that pin against whatever is already installed at `$KIN_HOME`:
43
+
44
+ - **installed is older than the pin** — upgrades it automatically (a one-line notice
45
+ on stderr, no prompt, no opt-in required).
46
+ - **installed is newer than the pin** — refuses the downgrade and exits with an
47
+ actionable error instead of running anything; re-run with `KIN_LAUNCHER_ADOPT=1` to
48
+ force it on purpose (e.g. deliberately pinning to an older release).
49
+ - **installed matches the pin** — runs it as-is.
50
+
51
+ `KIN_LAUNCHER_ADOPT=1` always forces a fresh provision of the pinned release, even when
52
+ the installed version already matches.
53
+
32
54
  ## Environment
33
55
 
34
56
  | Variable | Effect |
@@ -36,14 +58,21 @@ silently downgrades an install the other made.
36
58
  | `KIN_HOME` | Root of the managed install (default `~/.kin`). |
37
59
  | `KIN_MANAGED_BIN` | Explicit path to a `kin` binary; disables provisioning entirely. |
38
60
  | `KIN_NO_PROVISION=1` | Never touch the network; fail loud if no binary is present. |
39
- | `KIN_LAUNCHER_ADOPT=1` | Allow re-provisioning over a version-skewed non-npm install. |
61
+ | `KIN_LAUNCHER_ADOPT=1` | Force re-provisioning to the pinned release, including over a newer install that would otherwise be refused as a downgrade. |
40
62
 
41
63
  ## MCP setup
42
64
 
43
- Any MCP client can use the included server:
65
+ `kin setup --intent agent` is the preferred MCP setup path. It writes an absolute path to
66
+ the managed native `kin` binary, so agents do not depend on inheriting your shell `PATH`.
67
+
68
+ Any MCP client can also use the included server manually:
44
69
 
45
70
  ```json
46
- { "command": "npx", "args": ["-y", "@kinlab/kin", "mcp", "start"] }
71
+ {
72
+ "command": "npx",
73
+ "args": ["-y", "@kinlab/kin", "mcp", "start"],
74
+ "env": { "KIN_MCP_TOOL_PROFILE": "agent-default" }
75
+ }
47
76
  ```
48
77
 
49
78
  `@kinlab/kin-mcp` remains published for existing configurations; new setups should use
package/bin/kin-mcp.mjs CHANGED
@@ -26,14 +26,22 @@ if (argv.includes('--version') || argv.includes('-V')) {
26
26
  }
27
27
 
28
28
  let binary = null;
29
+ let provisionError = null;
29
30
  try {
30
31
  binary = await ensureProvisioned();
31
32
  } catch (error) {
32
- process.stderr.write(`kin-mcp: provisioning failed: ${error.message}\n`);
33
+ provisionError = error;
33
34
  }
34
35
 
35
36
  if (!binary) {
36
- process.stderr.write(`${notProvisionedMessage('kin')}\n`);
37
+ // The error message is already complete and actionable (e.g. a declined
38
+ // downgrade names the binary that IS present) — printing the generic
39
+ // not-provisioned message on top of it would contradict it.
40
+ if (provisionError) {
41
+ process.stderr.write(`kin-mcp: provisioning failed: ${provisionError.message}\n`);
42
+ } else {
43
+ process.stderr.write(`${notProvisionedMessage('kin')}\n`);
44
+ }
37
45
  process.exit(1);
38
46
  }
39
47
 
package/bin/kin.mjs CHANGED
@@ -33,13 +33,20 @@ try {
33
33
 
34
34
  if (!binary) {
35
35
  if (provisionError) {
36
+ // The error message is already complete and actionable (e.g. a declined
37
+ // downgrade names the binary that IS present) — printing the generic
38
+ // not-provisioned message on top of it would contradict it.
36
39
  process.stderr.write(`kin: provisioning failed: ${provisionError.message}\n`);
40
+ if (wantsVersion) {
41
+ process.stdout.write(launcherVersionLine(', provisioning failed'));
42
+ }
43
+ process.exit(1);
37
44
  }
38
45
  if (wantsVersion) {
39
46
  // Honest launcher identity; success only when provisioning was explicitly
40
47
  // disabled rather than broken.
41
- process.stdout.write(launcherVersionLine(provisionError ? ', provisioning failed' : ', not installed'));
42
- process.exit(provisionError ? 1 : 0);
48
+ process.stdout.write(launcherVersionLine(', not installed'));
49
+ process.exit(0);
43
50
  }
44
51
  process.stderr.write(`${notProvisionedMessage('kin')}\n`);
45
52
  process.exit(1);
package/lib/provision.mjs CHANGED
@@ -20,6 +20,7 @@ import { spawnSync } from 'node:child_process';
20
20
 
21
21
  import {
22
22
  binaryName,
23
+ compareVersions,
23
24
  kinHome,
24
25
  managedBinaryPath,
25
26
  readLauncherStamp,
@@ -196,16 +197,26 @@ export function probeBinaryVersion(binary, spawnImpl = spawnSync) {
196
197
  * Ensure the managed `kin` binary matching this launcher's pinned release is
197
198
  * present, provisioning (or re-provisioning) when needed. Returns the binary
198
199
  * path, or null when nothing usable exists and provisioning is unavailable.
200
+ * Throws when an installed release outranks the pin and the caller has not
201
+ * opted into a downgrade — this function never returns a value while quietly
202
+ * declining to honor the pin.
199
203
  *
200
204
  * Policy, in order:
201
205
  * - $KIN_MANAGED_BIN is an explicit user pin: use it as-is, never provision.
202
206
  * - $KIN_NO_PROVISION=1: resolve-only, no network.
203
- * - launcher stamp == target and the binary exists: run it (no probe cost).
204
- * - stamp differs or binary missing: provision the pinned release.
205
- * - no stamp but a binary exists (foreign install, e.g. install.sh): probe
206
- * its version; adopt on match. On mismatch respect the foreign install
207
- * run it with a one-line notice unless $KIN_LAUNCHER_ADOPT=1 opts into
208
- * re-provisioning. Never silently downgrade someone else's install.
207
+ * - $KIN_LAUNCHER_ADOPT=1: always (re-)provision the pinned release,
208
+ * regardless of what is already installed.
209
+ * - nothing installed yet: provision the pinned release.
210
+ * - installed version is known from the launcher stamp when this package
211
+ * provisioned it (no probe needed), otherwise by probing a foreign
212
+ * install's `--version` once:
213
+ * - installed == pinned: reuse it (a foreign match adopts the stamp).
214
+ * - installed < pinned: the pin is an upgrade — provision it
215
+ * automatically with a one-line notice. Never a silent no-op.
216
+ * - installed > pinned: the pin would downgrade a newer install — fail
217
+ * loud with an actionable message instead of running anything.
218
+ * - installed version unparseable (foreign binary that will not report a
219
+ * version): can't prove direction, so respect it — notice and reuse.
209
220
  */
210
221
  export async function ensureProvisioned(opts = {}) {
211
222
  const {
@@ -229,29 +240,37 @@ export async function ensureProvisioned(opts = {}) {
229
240
 
230
241
  const doProvision = () => provision(target, { env, platform, arch, fetchImpl, log });
231
242
 
243
+ if (env.KIN_LAUNCHER_ADOPT === '1') {
244
+ return doProvision();
245
+ }
232
246
  if (!existing) {
233
247
  return doProvision();
234
248
  }
235
249
 
250
+ const installedPath = managedBinaryPath('kin', env, platform);
236
251
  const stamp = readLauncherStamp(env);
237
- if (stamp === target) {
252
+ const installed = stamp ?? probeBinaryVersion(existing, spawnImpl);
253
+
254
+ if (installed === null) {
255
+ log(
256
+ `kin: using existing managed kin (version unknown) at ${installedPath} ` +
257
+ `(this @kinlab/kin pins ${target}; set KIN_LAUNCHER_ADOPT=1 to re-provision).`,
258
+ );
238
259
  return existing;
239
260
  }
240
- if (stamp !== null) {
241
- return doProvision();
242
- }
243
261
 
244
- const probed = probeBinaryVersion(existing, spawnImpl);
245
- if (probed === target) {
246
- writeLauncherStamp(target, env);
262
+ const cmp = compareVersions(target, installed);
263
+ if (cmp === 0) {
264
+ if (stamp === null) writeLauncherStamp(target, env);
247
265
  return existing;
248
266
  }
249
- if (env.KIN_LAUNCHER_ADOPT === '1') {
267
+ if (cmp > 0) {
268
+ log(`kin: managed kin ${installed} is older than the pinned ${target}; upgrading automatically.`);
250
269
  return doProvision();
251
270
  }
252
- log(
253
- `kin: using existing managed kin ${probed ?? 'unknown'} at ${managedBinaryPath('kin', env, platform)} ` +
254
- `(this @kinlab/kin pins ${target}; set KIN_LAUNCHER_ADOPT=1 to re-provision).`,
271
+ throw new Error(
272
+ `refusing to downgrade managed kin ${installed} to the pinned ${target} (at ${installedPath}). ` +
273
+ 'This @kinlab/kin would replace a newer managed install with an older one; nothing was run. ' +
274
+ 'Set KIN_LAUNCHER_ADOPT=1 to force it if the downgrade is intended.',
255
275
  );
256
- return existing;
257
276
  }
package/lib/resolve.mjs CHANGED
@@ -33,6 +33,73 @@ export function targetKinVersion() {
33
33
  return packageVersion();
34
34
  }
35
35
 
36
+ /**
37
+ * Compare two dotted version strings by semver precedence: numeric release
38
+ * segments first, then any `-prerelease` suffix (a release outranks its own
39
+ * prerelease; prerelease identifiers compare numerically when both sides are
40
+ * numeric, else lexically; on an equal shared prefix, fewer identifiers ranks
41
+ * lower). Mirrors the precedence `kin update` applies on the native side
42
+ * (crates/kin-cli/src/commands/update.rs). Returns -1 when `a` is older than
43
+ * `b`, 0 when equal, 1 when `a` is newer than `b`. Non-numeric release
44
+ * segments parse as 0 rather than throwing, so a malformed string degrades to
45
+ * a comparison instead of crashing the launcher.
46
+ */
47
+ export function compareVersions(a, b) {
48
+ const [aCore, aPre] = splitVersion(a);
49
+ const [bCore, bPre] = splitVersion(b);
50
+ const core = compareNumericParts(aCore, bCore);
51
+ return core !== 0 ? core : comparePrereleaseParts(aPre, bPre);
52
+ }
53
+
54
+ function splitVersion(version) {
55
+ const trimmed = String(version).trim().replace(/^v/, '');
56
+ const dash = trimmed.indexOf('-');
57
+ const core = dash === -1 ? trimmed : trimmed.slice(0, dash);
58
+ const pre = dash === -1 ? null : trimmed.slice(dash + 1).split('.');
59
+ const nums = core.split('.').map((part) => {
60
+ const n = Number.parseInt(part, 10);
61
+ return Number.isFinite(n) ? n : 0;
62
+ });
63
+ return [nums, pre];
64
+ }
65
+
66
+ function compareNumericParts(a, b) {
67
+ const len = Math.max(a.length, b.length);
68
+ for (let i = 0; i < len; i += 1) {
69
+ const x = a[i] ?? 0;
70
+ const y = b[i] ?? 0;
71
+ if (x !== y) return x < y ? -1 : 1;
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ function comparePrereleaseParts(a, b) {
77
+ if (a === null && b === null) return 0;
78
+ if (a === null) return 1; // a released version outranks its own prerelease
79
+ if (b === null) return -1;
80
+ const len = Math.max(a.length, b.length);
81
+ for (let i = 0; i < len; i += 1) {
82
+ if (i >= a.length) return -1; // shared prefix equal, fewer identifiers ranks lower
83
+ if (i >= b.length) return 1;
84
+ const cmp = comparePrereleaseIdentifier(a[i], b[i]);
85
+ if (cmp !== 0) return cmp;
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ function comparePrereleaseIdentifier(a, b) {
91
+ const aNumeric = /^\d+$/.test(a);
92
+ const bNumeric = /^\d+$/.test(b);
93
+ if (aNumeric && bNumeric) {
94
+ const x = Number.parseInt(a, 10);
95
+ const y = Number.parseInt(b, 10);
96
+ return x === y ? 0 : x < y ? -1 : 1;
97
+ }
98
+ if (aNumeric) return -1; // numeric identifiers rank below alphanumeric
99
+ if (bNumeric) return 1;
100
+ return a === b ? 0 : a < b ? -1 : 1;
101
+ }
102
+
36
103
  /**
37
104
  * Map a Node platform+arch pair to the Rust target triple of the managed
38
105
  * binary. Pure and injectable so it is testable for every supported host.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kinlab/kin",
3
- "version": "0.2.13",
4
- "description": "Canonical installer and launcher for Kin — provisions and runs the managed kin + kin-daemon release. MCP is one included mode (kin mcp start).",
3
+ "version": "0.2.14",
4
+ "description": "Canonical installer and launcher for Kin, the semantic system of record for software work — provisions and runs the managed kin + kin-daemon release. MCP is one included mode (kin mcp start).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {