@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 +2 -2
- package/src/cli/commands/cap/tokensave.ts +4 -4
- package/src/lib/public-npm.ts +2 -1
- package/src/lib/update-checker.ts +15 -9
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +9 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +9 -0
- package/src/tools/setup/capabilities/tokensave.ts +72 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.16.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
|
|
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
|
|
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
|
}
|
package/src/lib/public-npm.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
239
|
-
*
|
|
240
|
-
*
|
|
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 (
|
|
437
|
+
if (
|
|
438
|
+
!(await ensureTokensaveInstalled(targetPath, {
|
|
439
|
+
upgrade: ctx.mode === "setup",
|
|
440
|
+
}))
|
|
441
|
+
) {
|
|
375
442
|
return;
|
|
376
443
|
}
|
|
377
444
|
await ensureTokensaveBranchingPosture(targetPath);
|