total-recall-brain 3.41.3 → 3.41.5
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/bin/total-recall.mjs +12 -2
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/{index-DFenQQEQ.js → index-WT3gs-x6.js} +75 -70
- package/frontend/dist/index.html +1 -1
- package/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3131 -28
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3132 -29
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3164 -73
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +5937 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +172 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +619 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +3134 -31
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +413 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +502 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +35 -22
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +1225 -364
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3114 -83
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +5 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/config.mjs +3 -0
- package/src/cli/connect.mjs +12 -116
- package/src/cli/context.mjs +52 -23
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +0 -1
- package/src/cli/plugin/create.mjs +56 -3
- package/src/cli/plugin/index.mjs +15 -3
- package/src/cli/plugin/monetization.mjs +232 -12
- package/src/cli/preflight.mjs +161 -0
- package/src/cli/release.mjs +386 -0
- package/src/core/cli-commands.mjs +4 -0
- package/src/core/context-compactor.mjs +143 -0
- package/src/core/context-compiler.mjs +8 -1
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/meta-harness.mjs +16 -38
- package/src/core/plugin-dependencies.mjs +6 -1
- package/src/core/preflight-gate.mjs +77 -0
- package/src/core/preflight-receipt.mjs +123 -0
- package/src/core/runtime.mjs +0 -3
- package/src/core/surface.mjs +39 -2
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* total-recall release — publish a package, propagate it, and verify every consumer.
|
|
3
|
+
*
|
|
4
|
+
* The missing capability this fills: publishing to npm and verifying that the
|
|
5
|
+
* published artifact actually reached every registered consumer were manual
|
|
6
|
+
* steps outside any TR command. A release that is only on npm is not released,
|
|
7
|
+
* and a release whose consumers were never verified is a claim, not a fact.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx total-recall release plan <pkg> [--version X.Y.Z]
|
|
11
|
+
* npx total-recall release publish <pkg> [--version X.Y.Z] [--otp N]
|
|
12
|
+
* npx total-recall release propagate [--apply]
|
|
13
|
+
* npx total-recall release verify <pkg> [--version X.Y.Z]
|
|
14
|
+
* npx total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N]
|
|
15
|
+
*
|
|
16
|
+
* Never publishes without --apply. Never trusts the publish command's own
|
|
17
|
+
* report: verification reads the registry and each consumer's resolved
|
|
18
|
+
* installation independently.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { BRAIN_PACKAGES, updateAllBrains } from '../core/brain-updates.mjs';
|
|
25
|
+
import { listUpdateRoots, rootsFromEnv } from '../core/package-auto-update.mjs';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PACKAGES = BRAIN_PACKAGES;
|
|
28
|
+
|
|
29
|
+
/** @param {string[]} args */
|
|
30
|
+
export function parseArgs(args) {
|
|
31
|
+
const opts = {
|
|
32
|
+
action: null,
|
|
33
|
+
packages: [],
|
|
34
|
+
version: null,
|
|
35
|
+
apply: false,
|
|
36
|
+
otp: null,
|
|
37
|
+
json: false,
|
|
38
|
+
help: false,
|
|
39
|
+
root: null,
|
|
40
|
+
timeoutMs: 20 * 60 * 1000,
|
|
41
|
+
};
|
|
42
|
+
const positional = [];
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const a = args[i];
|
|
45
|
+
if (a === '--help' || a === '-h') opts.help = true;
|
|
46
|
+
else if (a === '--apply') opts.apply = true;
|
|
47
|
+
else if (a === '--json') opts.json = true;
|
|
48
|
+
else if (a === '--version' && args[i + 1]) opts.version = args[++i];
|
|
49
|
+
else if (a === '--otp' && args[i + 1]) opts.otp = args[++i];
|
|
50
|
+
else if (a === '--root' && args[i + 1]) opts.root = args[++i];
|
|
51
|
+
else if (a === '--timeout' && args[i + 1]) opts.timeoutMs = Number(args[++i]);
|
|
52
|
+
else if (!a.startsWith('-')) positional.push(a);
|
|
53
|
+
}
|
|
54
|
+
opts.action = positional.shift() || null;
|
|
55
|
+
opts.packages = positional;
|
|
56
|
+
return opts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ACTIONS = ['plan', 'publish', 'propagate', 'verify', 'all'];
|
|
60
|
+
|
|
61
|
+
function printHelp() {
|
|
62
|
+
console.log(`
|
|
63
|
+
total-recall release — publish, propagate and verify a package release
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
total-recall release plan <pkg> [--version X.Y.Z]
|
|
67
|
+
total-recall release publish <pkg> [--version X.Y.Z] [--otp N] --apply
|
|
68
|
+
total-recall release propagate [--apply] [--json]
|
|
69
|
+
total-recall release verify <pkg> [--version X.Y.Z] [--json]
|
|
70
|
+
total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N] [--json]
|
|
71
|
+
|
|
72
|
+
Actions:
|
|
73
|
+
plan Resolve the release version and report gate/consumer readiness. No writes.
|
|
74
|
+
publish Run the package's prepublish gate, then npm publish. Requires --apply.
|
|
75
|
+
propagate Pin the published version across every registered consumer (update --all-brains).
|
|
76
|
+
verify Independently confirm the registry artifact and each consumer's resolved install.
|
|
77
|
+
all plan → publish → propagate → verify, stopping at the first failure.
|
|
78
|
+
|
|
79
|
+
Rules enforced here:
|
|
80
|
+
• Publishing requires --apply; plan/verify never write.
|
|
81
|
+
• Verification never trusts the publish or propagate report: it re-reads the
|
|
82
|
+
npm registry and each consumer's own resolved package.json.
|
|
83
|
+
• A version that is only on npm is reported as incomplete, not as released.
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
--apply Perform writes (required for publish/propagate).
|
|
87
|
+
--version X.Y.Z Expect or assert a specific version (defaults to the source package.json).
|
|
88
|
+
--otp N npm one-time password, passed through to npm publish.
|
|
89
|
+
--root PATH Source repo for the package (defaults to $PWD).
|
|
90
|
+
--json Machine-readable output.
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read a package's source metadata without assuming a monorepo layout. */
|
|
95
|
+
export function readSourcePackage(root, pkg) {
|
|
96
|
+
const candidates = [path.join(root, 'package.json')];
|
|
97
|
+
// A workspace source may live one level down (packages/<dir>/package.json).
|
|
98
|
+
for (const dir of ['packages', 'apps']) {
|
|
99
|
+
const base = path.join(root, dir);
|
|
100
|
+
if (!fs.existsSync(base)) continue;
|
|
101
|
+
for (const entry of fs.readdirSync(base)) {
|
|
102
|
+
candidates.push(path.join(base, entry, 'package.json'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const file of candidates) {
|
|
106
|
+
if (!fs.existsSync(file)) continue;
|
|
107
|
+
try {
|
|
108
|
+
const pkgJson = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
109
|
+
if (pkgJson.name === pkg) return { file, dir: path.dirname(file), pkgJson };
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore malformed candidates rather than failing the whole release */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** npm registry read. This is the independent source of truth for "published". */
|
|
118
|
+
export function publishedVersion(pkg, { exec = spawnSync } = {}) {
|
|
119
|
+
const res = exec('npm', ['view', pkg, 'version'], { encoding: 'utf8', timeout: 60000 });
|
|
120
|
+
if (res.status !== 0) return { ok: false, version: null, error: (res.stderr || '').trim() || 'npm view failed' };
|
|
121
|
+
return { ok: true, version: (res.stdout || '').trim(), error: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Wait for a just-published version to become readable from the registry. */
|
|
125
|
+
export async function awaitPublished(pkg, version, { attempts = 8, delayMs = 20000, exec = spawnSync, sleep } = {}) {
|
|
126
|
+
const pause = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
127
|
+
let last = null;
|
|
128
|
+
for (let i = 0; i < attempts; i++) {
|
|
129
|
+
const read = publishedVersion(pkg, { exec });
|
|
130
|
+
last = read;
|
|
131
|
+
if (read.ok && read.version === version) return { ok: true, version: read.version, attempts: i + 1 };
|
|
132
|
+
if (i < attempts - 1) await pause(delayMs);
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, version: last?.version ?? null, error: last?.error ?? `registry still reports ${last?.version}`, attempts };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every consumer root TR knows about: the project registry plus TR_SYNC_REPOS.
|
|
139
|
+
* Used by verify so it never depends on the propagate step's own bookkeeping.
|
|
140
|
+
*/
|
|
141
|
+
export function consumerRoots(opts = {}) {
|
|
142
|
+
// Explicit roots are authoritative: passing them means "verify exactly these",
|
|
143
|
+
// which is what makes this callable from tests and from --root. Without them,
|
|
144
|
+
// fall back to the whole registered fleet plus TR_SYNC_REPOS.
|
|
145
|
+
const explicit = (opts.roots || []).filter(Boolean);
|
|
146
|
+
const source = explicit.length
|
|
147
|
+
? explicit
|
|
148
|
+
: [...listUpdateRoots(opts).map((row) => (typeof row === 'string' ? row : row.root)), ...rootsFromEnv()];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const root of source) {
|
|
152
|
+
if (!root) continue;
|
|
153
|
+
const resolved = path.resolve(root);
|
|
154
|
+
if (seen.has(resolved)) continue;
|
|
155
|
+
seen.add(resolved);
|
|
156
|
+
out.push(resolved);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Read the version a consumer actually resolves for a package (declared + installed). */
|
|
162
|
+
export function consumerState(root, pkg, version = null) {
|
|
163
|
+
const declaredFiles = [];
|
|
164
|
+
const pkgFile = path.join(root, 'package.json');
|
|
165
|
+
let declared = null;
|
|
166
|
+
if (fs.existsSync(pkgFile)) {
|
|
167
|
+
try {
|
|
168
|
+
const j = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
|
|
169
|
+
const all = { ...(j.dependencies || {}), ...(j.devDependencies || {}), ...(j.optionalDependencies || {}) };
|
|
170
|
+
declared = all[pkg] ?? null;
|
|
171
|
+
if (declared) declaredFiles.push(pkgFile);
|
|
172
|
+
} catch {
|
|
173
|
+
/* unreadable package.json is reported as unknown, not as satisfied */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!declared) return { root, declared: null, installed: null, satisfied: false, reason: 'not-declared' };
|
|
177
|
+
|
|
178
|
+
// Resolve the version a spec pins. Handles plain versions ("0.14.4"),
|
|
179
|
+
// npm: aliases ("npm:@scope/pkg@0.14.4") and ranges. A range is satisfied by
|
|
180
|
+
// the installed version rather than by string equality.
|
|
181
|
+
const spec = String(declared).trim();
|
|
182
|
+
let want = null;
|
|
183
|
+
let range = false;
|
|
184
|
+
if (spec.startsWith('npm:')) {
|
|
185
|
+
const afterAt = spec.slice(4);
|
|
186
|
+
const idx = afterAt.lastIndexOf('@');
|
|
187
|
+
want = idx > 0 ? afterAt.slice(idx + 1) : null;
|
|
188
|
+
} else if (/^[~^]|^[<>]=?|\|\||\s-\s|\*|x$/i.test(spec)) {
|
|
189
|
+
range = true;
|
|
190
|
+
} else {
|
|
191
|
+
want = spec;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const installedCandidates = [
|
|
195
|
+
path.join(root, 'node_modules', pkg, 'package.json'),
|
|
196
|
+
// npm: aliases install under the alias directory, which is what .bin shims run.
|
|
197
|
+
...aliasDirs(root, pkg),
|
|
198
|
+
];
|
|
199
|
+
let installed = null;
|
|
200
|
+
for (const file of installedCandidates) {
|
|
201
|
+
if (!fs.existsSync(file)) continue;
|
|
202
|
+
try {
|
|
203
|
+
installed = JSON.parse(fs.readFileSync(file, 'utf8')).version ?? null;
|
|
204
|
+
if (installed) break;
|
|
205
|
+
} catch {
|
|
206
|
+
/* keep looking */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const satisfied = range ? Boolean(installed) && installed === version : Boolean(want && installed === want);
|
|
210
|
+
return {
|
|
211
|
+
root,
|
|
212
|
+
declared,
|
|
213
|
+
want,
|
|
214
|
+
range,
|
|
215
|
+
installed,
|
|
216
|
+
satisfied,
|
|
217
|
+
reason: !installed ? 'not-installed' : satisfied ? 'ok' : 'installed-mismatch',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Alias install dirs for a package, e.g. @ssss/cli -> node_modules/@ssss/cli. */
|
|
222
|
+
function aliasDirs(root, pkg) {
|
|
223
|
+
const dirs = [];
|
|
224
|
+
const nm = path.join(root, 'node_modules');
|
|
225
|
+
if (!fs.existsSync(nm)) return dirs;
|
|
226
|
+
// Known alias pair for the SSSS CLI: the alias dir is what .bin/ssss executes.
|
|
227
|
+
if (pkg === '@gregiteen/ssss-cli') {
|
|
228
|
+
dirs.push(path.join(nm, '@ssss', 'cli', 'package.json'));
|
|
229
|
+
}
|
|
230
|
+
return dirs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function verifyConsumers(pkg, version, opts = {}) {
|
|
234
|
+
const roots = consumerRoots(opts);
|
|
235
|
+
const states = roots.map((root) => consumerState(root, pkg, version));
|
|
236
|
+
const relevant = states.filter((s) => s.declared);
|
|
237
|
+
const failing = relevant.filter((s) => !s.satisfied);
|
|
238
|
+
return {
|
|
239
|
+
pkg,
|
|
240
|
+
version,
|
|
241
|
+
roots_considered: roots.length,
|
|
242
|
+
consumers: relevant.length,
|
|
243
|
+
failing: failing.length,
|
|
244
|
+
failures: failing.map((s) => ({ root: s.root, declared: s.declared, installed: s.installed, reason: s.reason })),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runPrepublish(dir, pkgJson, { exec = spawnSync, timeoutMs } = {}) {
|
|
249
|
+
if (!pkgJson?.scripts?.prepublishOnly) return { ok: true, skipped: true };
|
|
250
|
+
const manager = fs.existsSync(path.join(dir, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm';
|
|
251
|
+
const res = exec(manager, ['run', 'prepublishOnly'], { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
|
|
252
|
+
return { ok: res.status === 0, skipped: false, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function doPublish(dir, { otp, exec = spawnSync, timeoutMs } = {}) {
|
|
256
|
+
const args = ['publish'];
|
|
257
|
+
if (otp) args.push('--otp', otp);
|
|
258
|
+
const res = exec('npm', args, { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
|
|
259
|
+
return { ok: res.status === 0, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string[]} argv full process argv (bin convention)
|
|
264
|
+
*/
|
|
265
|
+
export async function run(argv = process.argv, deps = {}) {
|
|
266
|
+
const opts = parseArgs(argv.slice(3));
|
|
267
|
+
const log = opts.json ? () => {} : (...a) => console.log(...a);
|
|
268
|
+
const emit = (obj) => { if (opts.json) console.log(JSON.stringify(obj, null, 2)); };
|
|
269
|
+
|
|
270
|
+
if (opts.help || !opts.action) {
|
|
271
|
+
printHelp();
|
|
272
|
+
if (!opts.action && !opts.help) process.exitCode = 1;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!ACTIONS.includes(opts.action)) {
|
|
276
|
+
console.error(`Unknown release action: ${opts.action}. Expected one of: ${ACTIONS.join(', ')}`);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const report = { action: opts.action, packages: {}, ok: false, steps: [] };
|
|
282
|
+
const fail = (message) => {
|
|
283
|
+
report.error = message;
|
|
284
|
+
report.ok = false;
|
|
285
|
+
if (opts.json) emit(report); else console.error(`\n ✗ ${message}\n`);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// propagate and verify act on the whole fleet and need no package argument.
|
|
290
|
+
if (opts.action === 'propagate') {
|
|
291
|
+
const dryRun = !opts.apply;
|
|
292
|
+
log(`\n Propagating ${DEFAULT_PACKAGES.join(' + ')} across every registered consumer${dryRun ? ' (plan)' : ''}...`);
|
|
293
|
+
const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
|
|
294
|
+
report.steps.push({ step: 'propagate', dryRun, summary });
|
|
295
|
+
report.ok = (summary.failed || 0) === 0;
|
|
296
|
+
if (opts.json) { emit(report); return; }
|
|
297
|
+
log(`\n roots: ${summary.root_count ?? '—'} consumers: ${summary.consumer_count ?? '—'} failed: ${summary.failed ?? 0}`);
|
|
298
|
+
if (dryRun) log(' Run with --apply to install.\n');
|
|
299
|
+
if (!report.ok) process.exitCode = 1;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const packages = opts.packages.length ? opts.packages : DEFAULT_PACKAGES;
|
|
304
|
+
if (!packages.length) return fail('No package given.');
|
|
305
|
+
|
|
306
|
+
for (const pkg of packages) {
|
|
307
|
+
const root = opts.root || process.cwd();
|
|
308
|
+
const source = readSourcePackage(root, pkg);
|
|
309
|
+
const entry = { pkg, root, source: source?.file ?? null };
|
|
310
|
+
report.packages[pkg] = entry;
|
|
311
|
+
|
|
312
|
+
if (opts.action === 'verify') {
|
|
313
|
+
const version = opts.version || source?.pkgJson?.version;
|
|
314
|
+
if (!version) return fail(`${pkg}: no --version and no source package.json found under ${root}`);
|
|
315
|
+
entry.version = version;
|
|
316
|
+
const reg = await awaitPublished(pkg, version, { attempts: 1, delayMs: 0 });
|
|
317
|
+
entry.registry = reg;
|
|
318
|
+
const consumers = verifyConsumers(pkg, version, { roots: opts.root ? [opts.root] : [] });
|
|
319
|
+
entry.consumers = consumers;
|
|
320
|
+
if (!reg.ok) return fail(`${pkg}@${version} is not readable from the npm registry (${reg.error}).`);
|
|
321
|
+
if (consumers.failing > 0) {
|
|
322
|
+
return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers not satisfied. First: ${consumers.failures[0].root} (${consumers.failures[0].installed ?? 'not installed'}).`);
|
|
323
|
+
}
|
|
324
|
+
log(` ✓ ${pkg}@${version} verified: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!source) return fail(`${pkg}: source package.json not found under ${root}. Pass --root <repo>.`);
|
|
329
|
+
const version = opts.version || source.pkgJson.version;
|
|
330
|
+
entry.version = version;
|
|
331
|
+
if (opts.version && source.pkgJson.version !== opts.version) {
|
|
332
|
+
return fail(`${pkg}: source is ${source.pkgJson.version} but --version ${opts.version} was requested. Bump the source first.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (opts.action === 'plan') {
|
|
336
|
+
const reg = publishedVersion(pkg);
|
|
337
|
+
entry.registry = reg;
|
|
338
|
+
entry.gates = source.pkgJson.scripts?.prepublishOnly ? 'present' : 'absent';
|
|
339
|
+
entry.alreadyPublished = reg.ok && reg.version === version;
|
|
340
|
+
log(` ${pkg}: source ${version}, registry ${reg.version ?? '—'}${entry.alreadyPublished ? ' (already published)' : ''}, prepublish gate ${entry.gates}`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (opts.action === 'publish' || opts.action === 'all') {
|
|
345
|
+
const reg = publishedVersion(pkg);
|
|
346
|
+
entry.registry_before = reg;
|
|
347
|
+
if (reg.ok && reg.version === version) {
|
|
348
|
+
log(` · ${pkg}@${version} already published — skipping publish`);
|
|
349
|
+
entry.published = 'already';
|
|
350
|
+
} else {
|
|
351
|
+
if (!opts.apply) return fail(`publish requires --apply (would publish ${pkg}@${version}).`);
|
|
352
|
+
const gate = runPrepublish(source.dir, source.pkgJson, { timeoutMs: opts.timeoutMs });
|
|
353
|
+
entry.gate = gate.ok ? 'passed' : 'failed';
|
|
354
|
+
report.steps.push({ step: 'prepublish', pkg, ok: gate.ok, skipped: gate.skipped });
|
|
355
|
+
if (!gate.ok) return fail(`${pkg}: prepublishOnly gate failed.\n${gate.output}`);
|
|
356
|
+
const pub = doPublish(source.dir, { otp: opts.otp, timeoutMs: opts.timeoutMs });
|
|
357
|
+
entry.publish_output = pub.output;
|
|
358
|
+
if (!pub.ok) return fail(`${pkg}: npm publish failed.\n${pub.output}`);
|
|
359
|
+
const confirmed = await (deps.awaitPublished || awaitPublished)(pkg, version, deps);
|
|
360
|
+
entry.registry_after = confirmed;
|
|
361
|
+
if (!confirmed.ok) return fail(`${pkg}@${version} published but the registry still reports ${confirmed.version}. Treat as NOT released.`);
|
|
362
|
+
entry.published = 'yes';
|
|
363
|
+
log(` ✓ ${pkg}@${version} published and confirmed on the registry`);
|
|
364
|
+
}
|
|
365
|
+
if (opts.action === 'publish') continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (opts.action === 'all') {
|
|
369
|
+
const dryRun = !opts.apply;
|
|
370
|
+
const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
|
|
371
|
+
report.steps.push({ step: 'propagate', pkg, summary });
|
|
372
|
+
if ((summary.failed || 0) > 0) return fail(`${pkg}: propagation reported ${summary.failed} failure(s).`);
|
|
373
|
+
const consumers = verifyConsumers(pkg, version, {});
|
|
374
|
+
entry.consumers = consumers;
|
|
375
|
+
if (consumers.failing > 0) {
|
|
376
|
+
return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers still not satisfied after propagation.`);
|
|
377
|
+
}
|
|
378
|
+
log(` ✓ ${pkg}@${version}: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
report.ok = true;
|
|
383
|
+
if (opts.json) emit(report);
|
|
384
|
+
else if (opts.action === 'plan') log('\n Plan only — nothing published or changed.\n');
|
|
385
|
+
else log('');
|
|
386
|
+
}
|
|
@@ -14,6 +14,7 @@ export const CLI_COMMANDS = {
|
|
|
14
14
|
restore: 'restore.mjs',
|
|
15
15
|
upgrade: 'upgrade.mjs',
|
|
16
16
|
update: 'update.mjs',
|
|
17
|
+
release: 'release.mjs',
|
|
17
18
|
friction: 'friction.mjs',
|
|
18
19
|
chat: 'chat.mjs',
|
|
19
20
|
status: 'status.mjs',
|
|
@@ -22,6 +23,9 @@ export const CLI_COMMANDS = {
|
|
|
22
23
|
edit: 'edit.mjs',
|
|
23
24
|
recall: 'recall.mjs',
|
|
24
25
|
context: 'context.mjs',
|
|
26
|
+
compact: 'compact.mjs',
|
|
27
|
+
preflight: 'preflight.mjs',
|
|
28
|
+
'tr-preflight': 'preflight.mjs',
|
|
25
29
|
'context-stats': 'context-stats.mjs',
|
|
26
30
|
rules: 'rules.mjs',
|
|
27
31
|
search: 'recall.mjs',
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Budgeted task capsules: rank rules, keep verbatim text, and account for every rule left out. */
|
|
2
|
+
|
|
3
|
+
import { estimateTokens } from './context-policy.mjs';
|
|
4
|
+
import { cosineSimilarity } from './embeddings.mjs';
|
|
5
|
+
|
|
6
|
+
const DUPLICATE_SIMILARITY = 0.92;
|
|
7
|
+
const REFERENCE_SHARE = 0.4;
|
|
8
|
+
const TOP_FULL_SHARE = 0.3;
|
|
9
|
+
const SUMMARY_MAX_CHARS = 200;
|
|
10
|
+
const SUMMARY_MIN_CHARS = 80;
|
|
11
|
+
// A period here does not end a sentence.
|
|
12
|
+
const SENTENCE_END = /(?<!\be\.g)(?<!\bi\.e)(?<!\bvs)[.!?](?=\s|$)/g;
|
|
13
|
+
|
|
14
|
+
/** The leading sentences of a rule, verbatim. Never rewords, because rewording can change a constraint. */
|
|
15
|
+
export function summarizeRule(text) {
|
|
16
|
+
const flat = String(text || '').replace(/^#+\s.*$/gm, ' ').replace(/\s+/g, ' ').trim();
|
|
17
|
+
if (flat.length <= SUMMARY_MAX_CHARS) return flat;
|
|
18
|
+
let end = 0;
|
|
19
|
+
for (const match of flat.matchAll(SENTENCE_END)) {
|
|
20
|
+
const candidate = match.index + 1;
|
|
21
|
+
if (candidate > SUMMARY_MAX_CHARS) break;
|
|
22
|
+
end = candidate;
|
|
23
|
+
if (end >= SUMMARY_MIN_CHARS) return flat.slice(0, end);
|
|
24
|
+
}
|
|
25
|
+
if (end) return flat.slice(0, end);
|
|
26
|
+
const cut = flat.lastIndexOf(' ', SUMMARY_MAX_CHARS);
|
|
27
|
+
return `${flat.slice(0, cut > 0 ? cut : SUMMARY_MAX_CHARS)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Relevance alone orders the text; ties fall to importance, then slug, so output is stable. */
|
|
31
|
+
function rank(items) {
|
|
32
|
+
return [...items].sort((a, b) =>
|
|
33
|
+
(b.score || 0) - (a.score || 0) ||
|
|
34
|
+
(b.importance || 3) - (a.importance || 3) ||
|
|
35
|
+
a.slug.localeCompare(b.slug));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findDuplicates(ranked) {
|
|
39
|
+
const duplicateOf = new Map();
|
|
40
|
+
const normalize = text => String(text).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
41
|
+
ranked.forEach((item, i) => {
|
|
42
|
+
if (item.priority === 'absolute') return;
|
|
43
|
+
for (const earlier of ranked.slice(0, i)) {
|
|
44
|
+
if (duplicateOf.has(earlier.slug)) continue;
|
|
45
|
+
const same = normalize(item.body) === normalize(earlier.body) ||
|
|
46
|
+
(item.vector && earlier.vector && cosineSimilarity(item.vector, earlier.vector) >= DUPLICATE_SIMILARITY);
|
|
47
|
+
if (same) { duplicateOf.set(item.slug, earlier.slug); return; }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return duplicateOf;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Assign each rule a tier so the printed output fits `budget`, measuring the real
|
|
55
|
+
* render after every change. `header(counts, tokens)` supplies the status lines.
|
|
56
|
+
*/
|
|
57
|
+
export function compactRules(items, { budget, expand = [], header, complete = false }) {
|
|
58
|
+
if (!Number.isFinite(budget) || budget < 1) throw new Error('Compact budget must be a positive token estimate');
|
|
59
|
+
const ranked = rank(items);
|
|
60
|
+
const known = new Set(ranked.map(item => item.slug));
|
|
61
|
+
const unknown = expand.filter(slug => !known.has(slug));
|
|
62
|
+
if (unknown.length) throw new Error(`Cannot expand rules not in this capsule: ${unknown.join(', ')}`);
|
|
63
|
+
const duplicateOf = findDuplicates(ranked.filter(item => !expand.includes(item.slug)));
|
|
64
|
+
const tiers = new Map(ranked.map(item => [item.slug, 'omitted']));
|
|
65
|
+
|
|
66
|
+
const render = () => {
|
|
67
|
+
const of = tier => ranked.filter(item => tiers.get(item.slug) === tier);
|
|
68
|
+
const full = of('full'), summary = of('summary'), reference = of('reference'), duplicate = of('duplicate');
|
|
69
|
+
const counts = { rules: ranked.length, full: full.length, summarized: summary.length, referenced: reference.length,
|
|
70
|
+
duplicate: duplicate.length, omitted: of('omitted').length };
|
|
71
|
+
const blocks = [
|
|
72
|
+
...full.map(item => item.text.trim()),
|
|
73
|
+
...summary.map(item => `[${item.slug}] ${summarizeRule(item.body)}`),
|
|
74
|
+
];
|
|
75
|
+
if (reference.length) blocks.push(`Referenced: ${reference.map(item => item.slug).join(', ')}`);
|
|
76
|
+
if (duplicate.length) blocks.push(`Duplicates: ${duplicate.map(item => `${item.slug} (of ${duplicateOf.get(item.slug)})`).join(', ')}`);
|
|
77
|
+
const body = blocks.join('\n\n---\n\n');
|
|
78
|
+
// The token count sits inside the text it counts, so iterate to a stable value.
|
|
79
|
+
let tokens = estimateTokens(body);
|
|
80
|
+
let context = '';
|
|
81
|
+
for (let i = 0; i < 5; i++) {
|
|
82
|
+
context = `${header(counts, tokens)}${body ? `\n${body}` : ''}`;
|
|
83
|
+
const measured = estimateTokens(`${context}\n`);
|
|
84
|
+
if (measured === tokens) break;
|
|
85
|
+
tokens = measured;
|
|
86
|
+
}
|
|
87
|
+
return { context, counts, tokens };
|
|
88
|
+
};
|
|
89
|
+
const used = () => render().tokens;
|
|
90
|
+
const tryTier = (item, tier, limit = budget) => {
|
|
91
|
+
const previous = tiers.get(item.slug);
|
|
92
|
+
tiers.set(item.slug, tier);
|
|
93
|
+
if (used() <= limit) return true;
|
|
94
|
+
tiers.set(item.slug, previous);
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// A complete capsule measures its own, higher floor below, so a retry at the reported number succeeds.
|
|
99
|
+
const floor = used();
|
|
100
|
+
if (!complete && floor > budget) {
|
|
101
|
+
const error = new Error(`Budget ${budget} cannot hold the capsule header; the minimum is ${floor}`);
|
|
102
|
+
error.requiredBudget = floor;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const slug of expand) {
|
|
107
|
+
if (!tryTier(ranked.find(item => item.slug === slug), 'full')) {
|
|
108
|
+
const error = new Error(`Budget ${budget} cannot hold expanded rule ${slug}; raise --budget or expand fewer rules`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A complete capsule accounts for every rule before ranking spends any budget, so
|
|
113
|
+
// ranking decides how much text a rule gets and never whether it applies.
|
|
114
|
+
if (complete) {
|
|
115
|
+
for (const item of ranked) {
|
|
116
|
+
if (tiers.get(item.slug) !== 'omitted') continue;
|
|
117
|
+
tiers.set(item.slug, duplicateOf.has(item.slug) ? 'duplicate' : item.priority === 'absolute' ? 'summary' : 'reference');
|
|
118
|
+
}
|
|
119
|
+
const completeFloor = used();
|
|
120
|
+
if (completeFloor > budget) {
|
|
121
|
+
const error = new Error(`Budget ${budget} cannot hold every rule; the minimum is ${completeFloor}`);
|
|
122
|
+
error.requiredBudget = completeFloor;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const candidates = ranked.filter(item => !duplicateOf.has(item.slug) && tiers.get(item.slug) !== 'full');
|
|
127
|
+
|
|
128
|
+
// Slugs alone must not consume a small budget: the top rules need their text.
|
|
129
|
+
const referenceLimit = used() + Math.floor((budget - used()) * REFERENCE_SHARE);
|
|
130
|
+
// Absolute rules are named first, so a small budget still shows they exist.
|
|
131
|
+
const unplaced = candidates.filter(item => tiers.get(item.slug) === 'omitted');
|
|
132
|
+
const namedFirst = [...unplaced.filter(item => item.priority === 'absolute'), ...unplaced.filter(item => item.priority !== 'absolute')];
|
|
133
|
+
for (const item of namedFirst) if (!tryTier(item, 'reference', referenceLimit)) break;
|
|
134
|
+
const topLimit = used() + Math.floor((budget - used()) * TOP_FULL_SHARE);
|
|
135
|
+
for (const item of candidates) tryTier(item, 'full', topLimit);
|
|
136
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'summary');
|
|
137
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'full');
|
|
138
|
+
for (const item of namedFirst) if (tiers.get(item.slug) === 'omitted') tryTier(item, 'reference');
|
|
139
|
+
for (const slug of duplicateOf.keys()) tryTier(ranked.find(item => item.slug === slug), 'duplicate');
|
|
140
|
+
|
|
141
|
+
const { context, counts, tokens } = render();
|
|
142
|
+
return { context, counts, tokens, tiers: Object.fromEntries(tiers) };
|
|
143
|
+
}
|
|
@@ -139,8 +139,15 @@ export async function compileContext({
|
|
|
139
139
|
return result;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Second line of every text capsule, so output that is truncated or piped through
|
|
144
|
+
* `head` still carries the rule (the rule once sat at line 544 of the output and
|
|
145
|
+
* an agent that cut the output at 80 lines never saw it).
|
|
146
|
+
*/
|
|
147
|
+
export const PREFLIGHT_NOTICE = 'PREFLIGHT: before any other tool call, read this whole capsule (never pipe or truncate it), then run: total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
|
|
148
|
+
|
|
142
149
|
export function renderCapsule(result, format = 'json') {
|
|
143
|
-
return format === 'text' ? `ready:${result.ready} tokens:${result.stats.total_tokens} version:${result.stats.version}\n${result.context}` : JSON.stringify(result);
|
|
150
|
+
return format === 'text' ? `ready:${result.ready} tokens:${result.stats.total_tokens} version:${result.stats.version}\n${PREFLIGHT_NOTICE}\n${result.context}` : JSON.stringify(result);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/** Budget the actual wire representation, including metadata, escaping and status. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Default runtime foundation and routine agent-development tools. */
|
|
2
2
|
export const DEFAULT_PLUGIN_IDS = Object.freeze([
|
|
3
|
-
'auth', 'mesh', 'openwiki', 'composable-cli', '
|
|
3
|
+
'auth', 'mesh', 'openwiki', 'composable-cli', 'meta-harness',
|
|
4
4
|
'code-quality', 'security', 'embeddings', 'git-sentinel',
|
|
5
5
|
]);
|
|
@@ -49,15 +49,6 @@ export const HARNESS_SPECS = {
|
|
|
49
49
|
execType: 'subcommand',
|
|
50
50
|
description: 'Autonomous program synthesis and sandbox execution.'
|
|
51
51
|
},
|
|
52
|
-
gemini: {
|
|
53
|
-
id: 'gemini',
|
|
54
|
-
name: 'Google Gemini CLI',
|
|
55
|
-
binary: 'gemini',
|
|
56
|
-
category: 'fast_utility',
|
|
57
|
-
defaultFlags: ['--sandbox=false', '--yolo', '-o', 'json'],
|
|
58
|
-
execType: 'flag_last',
|
|
59
|
-
description: 'High-speed utility completions and tool chaining.'
|
|
60
|
-
},
|
|
61
52
|
ollama: {
|
|
62
53
|
id: 'ollama',
|
|
63
54
|
name: 'Ollama Local LLM',
|
|
@@ -246,14 +237,6 @@ export async function detectHarnessesWithAuth({ readSecret = readStoreSecret } =
|
|
|
246
237
|
h.authed = true;
|
|
247
238
|
h.authRoute = 'cached-xai-auth';
|
|
248
239
|
}
|
|
249
|
-
} else if (h.id === 'gemini') {
|
|
250
|
-
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
251
|
-
h.authed = true;
|
|
252
|
-
h.authRoute = 'env-api-key';
|
|
253
|
-
} else if (fs.existsSync(path.join(os.homedir(), '.gemini'))) {
|
|
254
|
-
h.authed = true;
|
|
255
|
-
h.authRoute = 'cached-google-signin';
|
|
256
|
-
}
|
|
257
240
|
} else if (h.id === 'hermes') {
|
|
258
241
|
if (process.env.HERMES_API_KEY) {
|
|
259
242
|
h.authed = true;
|
|
@@ -471,6 +454,9 @@ export function getInstalledHarnessVersion(harnessId) {
|
|
|
471
454
|
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
472
455
|
if (m) return m[1];
|
|
473
456
|
} else if (harnessId === 'codex') {
|
|
457
|
+
const res = spawnSync('codex', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
458
|
+
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
459
|
+
if (m) return m[1];
|
|
474
460
|
const candidatePaths = [
|
|
475
461
|
path.join(os.homedir(), '.nvm/versions/node/v24.12.0/lib/node_modules/@openai/codex/package.json'),
|
|
476
462
|
'/usr/local/lib/node_modules/@openai/codex/package.json',
|
|
@@ -496,10 +482,6 @@ export function getInstalledHarnessVersion(harnessId) {
|
|
|
496
482
|
const res = spawnSync('ollama', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
497
483
|
const m = (res.stdout || res.stderr || '').match(/(\d+\.\d+\.\d+)/);
|
|
498
484
|
if (m) return m[1];
|
|
499
|
-
} else if (harnessId === 'gemini') {
|
|
500
|
-
const res = spawnSync('gemini', ['--version'], { timeout: 800, encoding: 'utf8' });
|
|
501
|
-
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
502
|
-
if (m) return m[1];
|
|
503
485
|
} else if (harnessId === 'hermes') {
|
|
504
486
|
const res = spawnSync('hermes', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
505
487
|
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
@@ -550,19 +532,24 @@ export async function getLatestHarnessVersion(harnessId, installedVersion = 'unk
|
|
|
550
532
|
if (d.version) latest = d.version;
|
|
551
533
|
}
|
|
552
534
|
} else if (harnessId === 'agy') {
|
|
553
|
-
|
|
535
|
+
try {
|
|
536
|
+
const res = spawnSync('agy', ['changelog'], { timeout: 2000, encoding: 'utf8' });
|
|
537
|
+
const m = (res.stdout || '').match(/^(\d+\.\d+\.\d+):/m);
|
|
538
|
+
if (m) latest = m[1];
|
|
539
|
+
else latest = installedVersion !== 'unknown' ? installedVersion : '1.4.12';
|
|
540
|
+
} catch {
|
|
541
|
+
latest = installedVersion !== 'unknown' ? installedVersion : '1.4.12';
|
|
542
|
+
}
|
|
554
543
|
} else if (harnessId === 'grok') {
|
|
555
|
-
latest = '0.2.118';
|
|
544
|
+
latest = installedVersion !== 'unknown' ? installedVersion : '0.2.118';
|
|
556
545
|
} else if (harnessId === 'ollama') {
|
|
557
|
-
latest = '
|
|
558
|
-
} else if (harnessId === 'gemini') {
|
|
559
|
-
latest = '0.3.0';
|
|
546
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
560
547
|
} else if (harnessId === 'hermes') {
|
|
561
|
-
latest = '
|
|
548
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
562
549
|
} else if (harnessId === 'dsh') {
|
|
563
|
-
latest = '
|
|
550
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
564
551
|
} else if (harnessId === 'openclaw') {
|
|
565
|
-
latest = '
|
|
552
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
566
553
|
}
|
|
567
554
|
} catch {}
|
|
568
555
|
|
|
@@ -677,15 +664,6 @@ export async function queryHarnessUsage() {
|
|
|
677
664
|
}
|
|
678
665
|
if (!isAuthed) quotaRemaining = 'unavailable — token expired (refresh needed)';
|
|
679
666
|
} catch {}
|
|
680
|
-
} else if (h.id === 'gemini') {
|
|
681
|
-
try {
|
|
682
|
-
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
683
|
-
if (fs.existsSync(accPath)) {
|
|
684
|
-
const acc = JSON.parse(fs.readFileSync(accPath, 'utf8'));
|
|
685
|
-
if (acc.active) identity = acc.active;
|
|
686
|
-
}
|
|
687
|
-
plan = 'Google AI API / Subscription';
|
|
688
|
-
} catch {}
|
|
689
667
|
} else if (h.id === 'ollama') {
|
|
690
668
|
identity = 'Local Machine';
|
|
691
669
|
plan = 'Offline Neural';
|