@uniqbit/mate-core 0.16.0-canary.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.16.0-canary.0",
3
+ "version": "0.16.0",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -50,7 +50,7 @@
50
50
  "@types/semver": "^7.7.1"
51
51
  },
52
52
  "engines": {
53
- "node": "24.x"
53
+ "node": ">=24"
54
54
  },
55
55
  "packageManager": "bun@1.3.14"
56
56
  }
@@ -6,7 +6,7 @@ import { ensureTokensaveInstalled } from "../../../tools/setup/capabilities/toke
6
6
 
7
7
  interface TokensaveCapDeps {
8
8
  resolveForLaunch?: typeof resolveForLaunch;
9
- ensureInstalled?: (repoPath: string) => Promise<boolean>;
9
+ ensureInstalled?: (repoPath: string, options?: { upgrade?: boolean }) => Promise<boolean>;
10
10
  spawn?: typeof spawnSync;
11
11
  }
12
12
 
@@ -15,8 +15,8 @@ interface TokensaveCapDeps {
15
15
  * @description Forwards `[...args]` to the `tokensave` CLI, run from the
16
16
  * active working repository. Requires the `tokensave` capability to be enabled
17
17
  * in `.mate/config/framework.yaml` and a registered working repo for the
18
- * current `MATE_REPO_ID`. Ensures the `tokensave` binary is installed in that
19
- * repo (via {@link ensureTokensaveInstalled}) before forwarding.
18
+ * current `MATE_REPO_ID`. Ensures the `tokensave` binary is installed and
19
+ * upgraded in that repo (via {@link ensureTokensaveInstalled}) before forwarding.
20
20
  * @remarks The child process's exit code is propagated via `process.exitCode`.
21
21
  */
22
22
  export async function runTokensaveCapCommand(
@@ -49,7 +49,7 @@ export async function runTokensaveCapCommand(
49
49
  return;
50
50
  }
51
51
 
52
- if (!(await ensureInstalled(repo.path))) {
52
+ if (!(await ensureInstalled(repo.path, { upgrade: true }))) {
53
53
  process.exitCode = 1;
54
54
  return;
55
55
  }
@@ -92,9 +92,10 @@ function withPublicNpmConfigSync<T>(
92
92
  export async function fetchPublicPackageVersion(
93
93
  packageName: string,
94
94
  registry = PUBLIC_NPM_REGISTRY,
95
+ distTag = "latest",
95
96
  ): Promise<string> {
96
97
  const { stdout } = await withPublicNpmConfig(packageName, registry, (env) =>
97
- publicNpmDeps.execFile("npm", ["view", packageName, "version"], {
98
+ publicNpmDeps.execFile("npm", ["view", `${packageName}@${distTag}`, "version"], {
98
99
  timeout: 10_000,
99
100
  env,
100
101
  }),
@@ -1,13 +1,13 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
+ import semver from "semver";
5
+
4
6
  import { getActiveDistribution, type DistributionUpdateConfig } from "../distribution";
5
7
  import { FRAMEWORK_NAME } from "../framework";
6
8
  import { fetchPublicPackageVersion, PUBLIC_NPM_REGISTRY } from "./public-npm";
7
9
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
8
10
 
9
- const parse = (v: string): number[] => v.split(".").slice(0, 3).map(Number);
10
-
11
11
  interface UpdateState {
12
12
  lastChecked: string;
13
13
  latestVersion: string | null;
@@ -31,11 +31,12 @@ const updateStateFileSlug = (packageName: string): string =>
31
31
  */
32
32
  export class UpdateStateStore extends YamlFileStore<UpdateState> {
33
33
  constructor(packageName: string = getUpdateConfig().packageName) {
34
+ const channelSuffix = getUpdateChannel() === "canary" ? "-canary" : "";
34
35
  super(
35
36
  path.join(
36
37
  os.homedir(),
37
38
  `.${FRAMEWORK_NAME}`,
38
- `update-state-${updateStateFileSlug(packageName)}.yaml`,
39
+ `update-state-${updateStateFileSlug(packageName)}${channelSuffix}.yaml`,
39
40
  ),
40
41
  );
41
42
  }
@@ -62,12 +63,15 @@ export function isCanaryVersion(version: string = getCurrentVersion()): boolean
62
63
  return version.includes("-canary");
63
64
  }
64
65
 
66
+ export type UpdateChannel = "latest" | "canary";
67
+
68
+ export function getUpdateChannel(version: string = getCurrentVersion()): UpdateChannel {
69
+ return isCanaryVersion(version) ? "canary" : "latest";
70
+ }
71
+
65
72
  export function isNewer(latest: string, current: string): boolean {
66
- const [la, lb, lc] = parse(latest);
67
- const [ca, cb, cc] = parse(current);
68
- if (la !== ca) return la > ca;
69
- if (lb !== cb) return lb > cb;
70
- return lc > cc;
73
+ if (!semver.valid(latest) || !semver.valid(current)) return false;
74
+ return semver.gt(latest, current);
71
75
  }
72
76
 
73
77
  export async function showUpdateBannerIfAvailable(store: UpdateStateStore): Promise<void> {
@@ -109,7 +113,9 @@ export async function enforceUpdateIfRequired(store: UpdateStateStore): Promise<
109
113
 
110
114
  export async function fetchLatestVersion(): Promise<string> {
111
115
  const { packageName, registry } = getUpdateConfig();
112
- return fetchPublicPackageVersion(packageName, registry);
116
+ const latestVersion = await fetchPublicPackageVersion(packageName, registry, getUpdateChannel());
117
+ if (!semver.valid(latestVersion)) throw new Error("npm returned an invalid version");
118
+ return latestVersion;
113
119
  }
114
120
 
115
121
  export function scheduleBackgroundCheck(store: UpdateStateStore): Promise<void> {
@@ -69,6 +69,9 @@ Ask exactly one focused question that would most reduce uncertainty. Attach your
69
69
  best guess about the answer and the reasoning behind it:
70
70
 
71
71
  ```text
72
+ CONFIDENCE: <0-100%> (<change since the previous answer>)
73
+ RESOLVED: <what is now clear>
74
+ REMAINING: <what is still uncertain>
72
75
  Q: <one focused question> GUESS: <your hypothesis for the answer and why>
73
76
  ```
74
77
 
@@ -76,6 +79,10 @@ Wait for the answer before asking the next question. Never batch questions or
76
79
  advance silently. The guess exposes assumptions and lets the user correct them
77
80
  quickly.
78
81
 
82
+ After every answer, update the confidence, resolved points, and remaining
83
+ uncertainty before asking the next question. Keep this progress state visible;
84
+ do not report confidence only at the beginning or end.
85
+
79
86
  ### Step 3: Listen for "want versus should want"
80
87
 
81
88
  Watch for best-practice talk without specifics, deference to convention, phrases
@@ -138,6 +145,8 @@ Before stopping, check that:
138
145
 
139
146
  - An initial hypothesis and confidence number were stated.
140
147
  - Every confidence number below 70 percent included its reason.
148
+ - Confidence, resolved points, and remaining uncertainty were shown before each
149
+ question.
141
150
  - Every question was asked one at a time with an attached guess.
142
151
  - The want-versus-should-want probe ran when the user gave a convention or
143
152
  sophistication-signaling answer.
@@ -69,6 +69,9 @@ Ask exactly one focused question that would most reduce uncertainty. Attach your
69
69
  best guess about the answer and the reasoning behind it:
70
70
 
71
71
  ```text
72
+ CONFIDENCE: <0-100%> (<change since the previous answer>)
73
+ RESOLVED: <what is now clear>
74
+ REMAINING: <what is still uncertain>
72
75
  Q: <one focused question> GUESS: <your hypothesis for the answer and why>
73
76
  ```
74
77
 
@@ -76,6 +79,10 @@ Wait for the answer before asking the next question. Never batch questions or
76
79
  advance silently. The guess exposes assumptions and lets the user correct them
77
80
  quickly.
78
81
 
82
+ After every answer, update the confidence, resolved points, and remaining
83
+ uncertainty before asking the next question. Keep this progress state visible;
84
+ do not report confidence only at the beginning or end.
85
+
79
86
  ### Step 3: Listen for "want versus should want"
80
87
 
81
88
  Watch for best-practice talk without specifics, deference to convention, phrases
@@ -138,6 +145,8 @@ Before stopping, check that:
138
145
 
139
146
  - An initial hypothesis and confidence number were stated.
140
147
  - Every confidence number below 70 percent included its reason.
148
+ - Confidence, resolved points, and remaining uncertainty were shown before each
149
+ question.
141
150
  - Every question was asked one at a time with an attached guess.
142
151
  - The want-versus-should-want probe ran when the user gave a convention or
143
152
  sophistication-signaling answer.
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
 
@@ -14,6 +15,8 @@ export const TOKENSAVE_STORE_CONFIG_FILE = "config.json";
14
15
  export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
15
16
  /** Single-graph posture Mate owns in the per-project store config; every other key is left as found. */
16
17
  const TOKENSAVE_OWNED_POSTURE = { auto_track: false, suppress_scope_warning: true } as const;
18
+ const TOKENSAVE_BRANCH_META_FILE = "branch-meta.json";
19
+ const TOKENSAVE_DISABLED_BRANCH_META_FILE = "branch-meta.json.mate-disabled";
17
20
  const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
18
21
  const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
19
22
  const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
@@ -104,6 +107,21 @@ async function tokensaveInstalled(repoPath: string): Promise<boolean> {
104
107
  return result.ok;
105
108
  }
106
109
 
110
+ /**
111
+ * Upgrades the installed binary without making setup depend on network
112
+ * availability. `--kill` keeps the non-interactive setup path from hanging on
113
+ * a running Tokensave MCP process.
114
+ */
115
+ async function upgradeTokensave(repoPath: string): Promise<void> {
116
+ const result = tokensaveDeps.run(["upgrade", "--kill"], repoPath);
117
+ if (result.ok) return;
118
+
119
+ const detail = result.stderr.trim();
120
+ process.stderr.write(
121
+ `tokensave: automatic upgrade failed${detail ? `: ${detail}` : ""} - continuing with the installed version\n`,
122
+ );
123
+ }
124
+
107
125
  // Global agent integration (MCP entry, session hooks, wildcard permission grant, and
108
126
  // tokensave's own config bookkeeping) is owned by tokensave's installer — Mate never
109
127
  // hand-edits ~/.tokensave/config.toml. Runs in setup mode only; the installer is
@@ -172,8 +190,12 @@ function getTokensaveInstallPlan(): { command: string; run: () => Promise<void>
172
190
  return undefined;
173
191
  }
174
192
 
175
- export async function ensureTokensaveInstalled(repoPath: string): Promise<boolean> {
193
+ export async function ensureTokensaveInstalled(
194
+ repoPath: string,
195
+ options: { upgrade?: boolean } = {},
196
+ ): Promise<boolean> {
176
197
  if (await tokensaveInstalled(repoPath)) {
198
+ if (options.upgrade) await upgradeTokensave(repoPath);
177
199
  return true;
178
200
  }
179
201
 
@@ -233,11 +255,49 @@ export async function ensureTokensaveStoreExcluded(repoPath: string): Promise<vo
233
255
  await fs.writeFile(excludePath, lines.join("\n") + "\n", "utf8");
234
256
  }
235
257
 
258
+ /**
259
+ * Deactivates Tokensave's multi-branch mode without deleting branch databases.
260
+ * The metadata is preserved in a Mate-owned backup so the migration is reversible.
261
+ */
262
+ async function deactivateTokensaveBranchMetadata(repoPath: string): Promise<void> {
263
+ const storePath = path.join(repoPath, TOKENSAVE_STORE_DIR);
264
+ const branchMetaPath = path.join(storePath, TOKENSAVE_BRANCH_META_FILE);
265
+ const disabledBranchMetaPath = path.join(storePath, TOKENSAVE_DISABLED_BRANCH_META_FILE);
266
+
267
+ let branchMeta: string;
268
+ try {
269
+ branchMeta = await fs.readFile(branchMetaPath, "utf8");
270
+ } catch {
271
+ return;
272
+ }
273
+
274
+ let removeActiveMetadata = false;
275
+ try {
276
+ await fs.copyFile(branchMetaPath, disabledBranchMetaPath, fsConstants.COPYFILE_EXCL);
277
+ removeActiveMetadata = true;
278
+ } catch (error) {
279
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
280
+ return;
281
+ }
282
+
283
+ try {
284
+ removeActiveMetadata = (await fs.readFile(disabledBranchMetaPath, "utf8")) === branchMeta;
285
+ } catch {
286
+ return;
287
+ }
288
+ }
289
+
290
+ if (removeActiveMetadata) {
291
+ await fs.unlink(branchMetaPath).catch(() => {});
292
+ }
293
+ }
294
+
236
295
  /**
237
296
  * Reconciles Mate's single-graph branching posture into the working repo's
238
- * git-excluded `.tokensave/config.json`. Best-effort by design: the file is
239
- * created by `tokensave init`, so an absent, unreadable, or unparseable config
240
- * is left untouched rather than treated as an error.
297
+ * git-excluded `.tokensave/config.json` and deactivates existing branch
298
+ * metadata. Best-effort by design: the file is created by `tokensave init`,
299
+ * so an absent, unreadable, or unparseable config is left untouched rather
300
+ * than treated as an error. Existing branch databases are never removed.
241
301
  */
242
302
  export async function ensureTokensaveBranchingPosture(repoPath: string): Promise<void> {
243
303
  const configPath = path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_CONFIG_FILE);
@@ -266,6 +326,7 @@ export async function ensureTokensaveBranchingPosture(repoPath: string): Promise
266
326
  );
267
327
  const serialized = `${next}\n`;
268
328
  if (serialized === raw) {
329
+ await deactivateTokensaveBranchMetadata(repoPath);
269
330
  return;
270
331
  }
271
332
 
@@ -274,6 +335,8 @@ export async function ensureTokensaveBranchingPosture(repoPath: string): Promise
274
335
  } catch {
275
336
  /** Best-effort: the store config is not Mate's to guarantee. */
276
337
  }
338
+
339
+ await deactivateTokensaveBranchMetadata(repoPath);
277
340
  }
278
341
 
279
342
  async function teardownDriver(repoPath: string, providers: string[]) {
@@ -371,7 +434,11 @@ export function createTokensavePlugin(): CapabilityPlugin {
371
434
  if (ctx.activeProviders.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p)).length === 0) return;
372
435
 
373
436
  const targetPath = ctx.repoPath ?? ctx.companionPath;
374
- if (!(await ensureTokensaveInstalled(targetPath))) {
437
+ if (
438
+ !(await ensureTokensaveInstalled(targetPath, {
439
+ upgrade: ctx.mode === "setup",
440
+ }))
441
+ ) {
375
442
  return;
376
443
  }
377
444
  await ensureTokensaveBranchingPosture(targetPath);