@tpsdev-ai/flair 0.54.1 → 0.54.2
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/dist/build-info.json +3 -3
- package/dist/commands/doctor.js +12 -1
- package/dist/commands/status.js +18 -1
- package/dist/commands/upgrade.js +41 -5
- package/dist/engine-version.js +12 -4
- package/dist/fabric-upgrade.js +30 -15
- package/dist/lib/npm-registry.js +578 -0
- package/dist/resources/mcp-tools.js +8 -3
- package/{node_modules/@tpsdev-ai/flair-tool-descriptors/dist → dist/resources/tool-descriptors}/index.js +4 -0
- package/dist/version-check.js +29 -8
- package/docs/releasing.md +17 -11
- package/package.json +4 -9
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +0 -19
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +0 -22
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +0 -70
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +0 -46
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.54.
|
|
3
|
-
"commit": "
|
|
4
|
-
"builtAt": "2026-09-
|
|
2
|
+
"version": "0.54.2",
|
|
3
|
+
"commit": "842bd1a5903663aabf48e6629b3b5ba17f0884b8",
|
|
4
|
+
"builtAt": "2026-09-15T18:02:12.387Z",
|
|
5
5
|
"builder": "tsc"
|
|
6
6
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -12,7 +12,8 @@ import { opsApiBindFinding } from "../lib/ops-api-bind.js";
|
|
|
12
12
|
import { flairCliVersion, unpinnedSpecWarning } from "../lib/mcp-spec.js";
|
|
13
13
|
import { staleSessionStartHookPins } from "../lib/owned-pins.js";
|
|
14
14
|
import * as render from "../render.js";
|
|
15
|
-
import { checkVersion, formatVersionNudge, probeInstanceVersion } from "../version-check.js";
|
|
15
|
+
import { checkVersion, formatVersionNudge, probeInstanceVersion, FLAIR_PKG_NAME } from "../version-check.js";
|
|
16
|
+
import { resolveRegistryNotice } from "../lib/npm-registry.js";
|
|
16
17
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
17
18
|
import { homedir } from "node:os";
|
|
18
19
|
import { dirname, join } from "node:path";
|
|
@@ -187,6 +188,16 @@ export function register(program) {
|
|
|
187
188
|
`Commands run through the CLI; the instance serves the data.`)}`);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
// flair#1692: name the registry (and where it came from) on every doctor
|
|
192
|
+
// check, so a redirected mirror is visible to the operator.
|
|
193
|
+
const registryNotice = await resolveRegistryNotice(FLAIR_PKG_NAME);
|
|
194
|
+
if (registryNotice.line) {
|
|
195
|
+
console.log(` ${render.icons.info} ${render.wrap(render.c.dim, registryNotice.line)}`);
|
|
196
|
+
}
|
|
197
|
+
if (registryNotice.error) {
|
|
198
|
+
console.log(` ${render.icons.warn} ${render.wrap(render.c.yellow, registryNotice.error)}`);
|
|
199
|
+
issues++;
|
|
200
|
+
}
|
|
190
201
|
// 0.5 npm global bin dir on PATH (flair#1134) — a user-prefix
|
|
191
202
|
// `npm i -g` succeeds and then `flair` is command-not-found because
|
|
192
203
|
// <prefix>/bin never made it into PATH. postinstall warns at install
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveAdminUser } from "../lib/auth-resolve.js";
|
|
2
2
|
import { opsApiBindFinding } from "../lib/ops-api-bind.js";
|
|
3
3
|
import * as render from "../render.js";
|
|
4
|
-
import { checkVersion, formatVersionNudge } from "../version-check.js";
|
|
4
|
+
import { checkVersion, formatVersionNudge, FLAIR_PKG_NAME } from "../version-check.js";
|
|
5
|
+
import { resolveRegistryNotice } from "../lib/npm-registry.js";
|
|
5
6
|
let cli;
|
|
6
7
|
/** Bind the cli-locals this module depends on. */
|
|
7
8
|
export function bindCli(fns) {
|
|
@@ -193,6 +194,10 @@ export function register(program) {
|
|
|
193
194
|
// fact is current (flair#1341). Independent of Harper health; runs either way.
|
|
194
195
|
const versionCheckResult = await checkVersion(__pkgVersion);
|
|
195
196
|
const versionNudge = formatVersionNudge(versionCheckResult);
|
|
197
|
+
// flair#1692: name the registry (and where it came from) on every status
|
|
198
|
+
// check, so a redirected mirror is never silent — including when the
|
|
199
|
+
// version answer came from cache and no network request was made.
|
|
200
|
+
const registryNotice = await resolveRegistryNotice(FLAIR_PKG_NAME);
|
|
196
201
|
if (opts.json) {
|
|
197
202
|
const out = { healthy, url: baseUrl, flairVersion: __pkgVersion, ...healthData };
|
|
198
203
|
if (localWarnings.length > 0) {
|
|
@@ -205,6 +210,10 @@ export function register(program) {
|
|
|
205
210
|
out.discoveredPort = discoveredPort;
|
|
206
211
|
if (versionCheckResult.latest)
|
|
207
212
|
out.latestVersion = versionCheckResult.latest;
|
|
213
|
+
if (registryNotice.line)
|
|
214
|
+
out.registry = registryNotice.line;
|
|
215
|
+
if (registryNotice.error)
|
|
216
|
+
out.registryError = registryNotice.error;
|
|
208
217
|
console.log(JSON.stringify(out, null, 2));
|
|
209
218
|
if (!healthy)
|
|
210
219
|
process.exit(1);
|
|
@@ -213,6 +222,10 @@ export function register(program) {
|
|
|
213
222
|
if (!healthy) {
|
|
214
223
|
console.log(`Flair v${__pkgVersion} — 🔴 unreachable`);
|
|
215
224
|
console.log(` URL: ${baseUrl}`);
|
|
225
|
+
if (registryNotice.line)
|
|
226
|
+
console.log(` ${registryNotice.line}`);
|
|
227
|
+
if (registryNotice.error)
|
|
228
|
+
console.log(` ⚠ ${registryNotice.error}`);
|
|
216
229
|
if (discoveredPort != null) {
|
|
217
230
|
const altUrl = `http://127.0.0.1:${discoveredPort}`;
|
|
218
231
|
console.log(`\n ⚠ Found a Flair daemon listening on port ${discoveredPort} (URL: ${altUrl}).`);
|
|
@@ -288,6 +301,10 @@ export function register(program) {
|
|
|
288
301
|
const metaParts = [pidPart, uptimePart].filter(Boolean).join(render.wrap(render.c.dim, " · "));
|
|
289
302
|
console.log(`${versionStr} ${render.wrap(render.c.dim, "—")} ${runStatus}${metaParts ? ` ${metaParts}` : ""}`);
|
|
290
303
|
console.log(render.kv("URL", baseUrl));
|
|
304
|
+
if (registryNotice.line)
|
|
305
|
+
console.log(render.kv("Registry", registryNotice.line.replace(/^registry:\s*/, "")));
|
|
306
|
+
if (registryNotice.error)
|
|
307
|
+
console.log(` ${render.icons.warn} ${render.wrap(render.c.yellow, registryNotice.error)}`);
|
|
291
308
|
if (versionNudge) {
|
|
292
309
|
const color = versionNudge.severity === "red" ? render.c.red : render.c.yellow;
|
|
293
310
|
console.log(`\n ${render.wrap(color, "⚠")} ${render.wrap(color, versionNudge.message)}`);
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -7,6 +7,7 @@ import { defaultKeysDir } from "../lib/auth-resolve.js";
|
|
|
7
7
|
import { renderVerifiedSummary } from "../lib/doctor-run.js";
|
|
8
8
|
import { isDetached, renderDetachedWarning } from "../lib/launchd-management.js";
|
|
9
9
|
import { FLAIR_MCP_PACKAGE, clearFlairCliVersionCache } from "../lib/mcp-spec.js";
|
|
10
|
+
import { createRegistryNoticePrinter, fetchLatestVersion, isStrictSemver } from "../lib/npm-registry.js";
|
|
10
11
|
import { ownedPinRefreshShouldReport, refreshOwnedPins } from "../lib/owned-pins.js";
|
|
11
12
|
import { extractSnapshotSafely, validateSnapshotArchive } from "../lib/safe-snapshot-extract.js";
|
|
12
13
|
import { collectUpgradeExecPathWarning, findFlairPackageDir, resolveNpmGlobalFlairPackage, resolveServingFlairPackage } from "../lib/upgrade-exec-path.js";
|
|
@@ -836,17 +837,36 @@ export function register(program) {
|
|
|
836
837
|
},
|
|
837
838
|
];
|
|
838
839
|
const findings = [];
|
|
840
|
+
// flair#1692: name the registry (and where it came from) the moment it is
|
|
841
|
+
// resolved, so a redirected registry is visible to the operator before
|
|
842
|
+
// anything is fetched or installed. One line per distinct registry.
|
|
843
|
+
const noticeRegistry = createRegistryNoticePrinter();
|
|
839
844
|
for (const { name, probe, kind, transitive } of packages) {
|
|
840
845
|
if (transitive && !showAll)
|
|
841
846
|
continue;
|
|
842
847
|
try {
|
|
843
848
|
let registryLatest = null;
|
|
844
849
|
try {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
850
|
+
// flair#1688: resolve the registry npm is configured to use for this
|
|
851
|
+
// package (scope mapping + .npmrc + env) instead of a hardcoded host.
|
|
852
|
+
// flair#1692: print it, refuse disallowed schemes, disable redirects,
|
|
853
|
+
// and validate the returned value as strict semver before it can be
|
|
854
|
+
// used as an `npm install` spec.
|
|
855
|
+
const lookup = await fetchLatestVersion(name, {
|
|
856
|
+
timeoutMs: 5000,
|
|
857
|
+
onRegistry: noticeRegistry,
|
|
858
|
+
});
|
|
859
|
+
if (lookup.kind === "ok") {
|
|
860
|
+
registryLatest = lookup.version;
|
|
861
|
+
}
|
|
862
|
+
else if (lookup.kind === "invalid") {
|
|
863
|
+
console.error(` ⚠ ${name}: registry returned a non-semver "latest" (${JSON.stringify(lookup.value)}) ` +
|
|
864
|
+
`from ${lookup.registry.url} — refusing to use it as an install spec.`);
|
|
849
865
|
}
|
|
866
|
+
else if (lookup.kind === "refused") {
|
|
867
|
+
console.error(lookup.message);
|
|
868
|
+
}
|
|
869
|
+
// kind === "unavailable": offline/timed out — the pin path must still work.
|
|
850
870
|
}
|
|
851
871
|
catch { /* /latest timed out or failed — pin path must still work */ }
|
|
852
872
|
let latest;
|
|
@@ -867,7 +887,14 @@ export function register(program) {
|
|
|
867
887
|
continue;
|
|
868
888
|
latest = registryLatest;
|
|
869
889
|
}
|
|
870
|
-
|
|
890
|
+
// flair#1692: a non-semver target must never reach an install spec
|
|
891
|
+
// (npm treats `pkg@<url>` as a remote tarball). This also covers the
|
|
892
|
+
// operator pin on the plain-tree lane.
|
|
893
|
+
if (!isStrictSemver(latest)) {
|
|
894
|
+
console.error(` ⚠ ${name}: refusing non-semver install target ${JSON.stringify(latest)} — expected a version like 1.2.3.`);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (name === FLAIR_PKG_NAME) {
|
|
871
898
|
try {
|
|
872
899
|
primeVersionCheckCache(latest);
|
|
873
900
|
}
|
|
@@ -1225,6 +1252,15 @@ export function register(program) {
|
|
|
1225
1252
|
let flairInstallFailed = false;
|
|
1226
1253
|
for (const { pkg, latest } of npmUpgrades) {
|
|
1227
1254
|
try {
|
|
1255
|
+
// flair#1692 backstop: the listing validated this, but the install is
|
|
1256
|
+
// the point of no return. npm accepts `pkg@<url>` as a remote-tarball
|
|
1257
|
+
// spec, so a non-semver target must never reach this argv.
|
|
1258
|
+
if (!isStrictSemver(latest)) {
|
|
1259
|
+
console.error(` ❌ ${pkg} upgrade skipped: non-semver target ${JSON.stringify(latest)}`);
|
|
1260
|
+
if (pkg === FLAIR_PKG_NAME)
|
|
1261
|
+
flairInstallFailed = true;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1228
1264
|
if (treePlan && pkg === FLAIR_PKG_NAME) {
|
|
1229
1265
|
console.log(` Fetching ${pkg}@${latest} (npm pack) and swapping ${treePlan.treeDir}...`);
|
|
1230
1266
|
await applyPlainTreeUpgrade(treePlan);
|
package/dist/engine-version.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { fetchDeclaredDependencies } from "./lib/npm-registry.js";
|
|
22
23
|
/** Filename of the engine-version stamp inside the data directory. */
|
|
23
24
|
export const ENGINE_VERSION_STAMP = "engine-version.txt";
|
|
24
25
|
/** Root directory for pre-upgrade snapshots (~/.flair/upgrade-snapshots). */
|
|
@@ -47,11 +48,18 @@ export function readInstalledHarperVersion(packageRoot) {
|
|
|
47
48
|
*/
|
|
48
49
|
export async function fetchDeclaredHarperVersion(flairVersion) {
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// flair#1688: resolve the configured registry rather than assuming npmjs —
|
|
52
|
+
// the engine-version decision must look at the same package the upgrade
|
|
53
|
+
// will actually install. flair#1692: the same scheme allowlist, redirect
|
|
54
|
+
// refusal, npm transport, and non-default-registry handling apply.
|
|
55
|
+
const result = await fetchDeclaredDependencies("@tpsdev-ai/flair", flairVersion, { timeoutMs: 5000 });
|
|
56
|
+
if (result.kind === "refused") {
|
|
57
|
+
console.error(result.message);
|
|
52
58
|
return null;
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
}
|
|
60
|
+
if (result.kind !== "ok")
|
|
61
|
+
return null;
|
|
62
|
+
return result.dependencies?.harper ?? result.dependencies?.["@harperfast/harper"] ?? null;
|
|
55
63
|
}
|
|
56
64
|
catch {
|
|
57
65
|
return null;
|
package/dist/fabric-upgrade.js
CHANGED
|
@@ -30,6 +30,7 @@ import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "no
|
|
|
30
30
|
import { join } from "node:path";
|
|
31
31
|
import { tmpdir } from "node:os";
|
|
32
32
|
import { createRequire } from "node:module";
|
|
33
|
+
import { createRegistryNoticePrinter, fetchDeclaredDependencies, fetchLatestVersion } from "./lib/npm-registry.js";
|
|
33
34
|
/**
|
|
34
35
|
* Minimum harper version whose component packager works when the
|
|
35
36
|
* package root is under node_modules (flair#513 — the empty-tarball fix landed
|
|
@@ -154,30 +155,44 @@ export function buildDeployablePackageJson(flairVersion, pin) {
|
|
|
154
155
|
return pkg;
|
|
155
156
|
}
|
|
156
157
|
// ─── Default (real) dependency implementations ──────────────────────────────
|
|
157
|
-
|
|
158
|
+
// flair#1692: one printer per process, so a Fabric upgrade names the registry
|
|
159
|
+
// (and source) it stages from without repeating the line per lookup.
|
|
160
|
+
const noticeRegistry = createRegistryNoticePrinter();
|
|
158
161
|
async function defaultFetchLatestFlairVersion() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// flair#1688: the configured registry (scoped `@tpsdev-ai:registry`,
|
|
163
|
+
// project/user/global .npmrc, env `npm_config_registry`), not a hardcoded
|
|
164
|
+
// host — a Fabric upgrade must stage the package from the mirror the
|
|
165
|
+
// operator configured, or fail rather than silently use the public one.
|
|
166
|
+
// flair#1692: the value is validated as strict semver before it is used as
|
|
167
|
+
// `dependencies` in the staged package.json (npm accepts `pkg@<url>` as a
|
|
168
|
+
// remote-tarball spec, so an unvalidated value is an arbitrary-install
|
|
169
|
+
// primitive).
|
|
170
|
+
const result = await fetchLatestVersion(FLAIR_PKG, { timeoutMs: 10_000, onRegistry: noticeRegistry });
|
|
171
|
+
if (result.kind === "ok")
|
|
172
|
+
return result.version;
|
|
173
|
+
if (result.kind === "refused")
|
|
174
|
+
throw new Error(result.message);
|
|
175
|
+
if (result.kind === "invalid") {
|
|
176
|
+
throw new Error(`npm registry returned a non-semver latest version (${JSON.stringify(result.value)}) for ${FLAIR_PKG} ` +
|
|
177
|
+
`from ${result.registry.url} (source: ${result.registry.source}) — refusing to use it`);
|
|
164
178
|
}
|
|
165
|
-
|
|
166
|
-
if (!data.version)
|
|
167
|
-
throw new Error(`No version in registry response for ${FLAIR_PKG}`);
|
|
168
|
-
return data.version;
|
|
179
|
+
throw new Error(`Could not determine ${FLAIR_PKG}/latest: ${result.message}`);
|
|
169
180
|
}
|
|
170
181
|
async function defaultFetchDeclaredHarperVersion(flairVersion) {
|
|
171
|
-
const
|
|
172
|
-
|
|
182
|
+
const result = await fetchDeclaredDependencies(FLAIR_PKG, flairVersion, {
|
|
183
|
+
timeoutMs: 10_000,
|
|
184
|
+
onRegistry: noticeRegistry,
|
|
173
185
|
});
|
|
174
|
-
if (
|
|
186
|
+
if (result.kind === "refused") {
|
|
187
|
+
console.error(result.message);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (result.kind !== "ok")
|
|
175
191
|
return null;
|
|
176
|
-
const data = (await res.json());
|
|
177
192
|
// Either package name (flair#870) — pre-rename flair versions declare the
|
|
178
193
|
// scoped one, and those are precisely the versions that may need an override.
|
|
179
194
|
for (const name of HARPER_PKG_NAMES) {
|
|
180
|
-
const declared =
|
|
195
|
+
const declared = result.dependencies?.[name];
|
|
181
196
|
if (declared)
|
|
182
197
|
return declared;
|
|
183
198
|
}
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm-registry.ts — resolve the npm registry to query for a package, the way
|
|
3
|
+
* npm itself would, and fetch from it with npm's security boundaries applied
|
|
4
|
+
* (flair#1688, security review flair#1692).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. `flair upgrade`'s update check and Fabric version lookups
|
|
7
|
+
* used to fetch `https://registry.npmjs.org/<pkg>/latest` with a HARDCODED
|
|
8
|
+
* host. A user on a private mirror, an air-gapped registry, or a vetted
|
|
9
|
+
* internal proxy configured through npm never influenced those fetches: the
|
|
10
|
+
* upgrade path compared against the public registry's `latest` (reporting
|
|
11
|
+
* "you are current" when the mirror had a different/newer release) and, in CI,
|
|
12
|
+
* defeated the scoped `@tpsdev-ai:registry` config the macOS launchd lane sets.
|
|
13
|
+
* A supply-chain control (route all installs through the internal mirror) was
|
|
14
|
+
* bypassed by the update check itself.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT DOES. Given a package name, returns the registry base URL npm would
|
|
17
|
+
* use for it, honouring the same configuration npm does:
|
|
18
|
+
*
|
|
19
|
+
* 1. the `@<scope>:registry` mapping for a scoped package (scope-specific
|
|
20
|
+
* config beats the default for that scope),
|
|
21
|
+
* 2. the default `registry`,
|
|
22
|
+
* 3. npm's own precedence: env `npm_config_<key>` > project `.npmrc` > user
|
|
23
|
+
* `.npmrc` > global `.npmrc` > npm's builtin default.
|
|
24
|
+
*
|
|
25
|
+
* Rather than reimplement npm's ini parsing and precedence (which would surely
|
|
26
|
+
* drift from npm), we ask npm: `npm config list` already resolves every layer
|
|
27
|
+
* in the right order, and — unlike `npm config get` — its per-section headers
|
|
28
|
+
* let us name the layer a value came from, so the operator can SEE a
|
|
29
|
+
* redirected registry (`registry: … (source: user .npmrc …)`). The env layer
|
|
30
|
+
* is read directly too, as a fast path so tests (and scripts that export
|
|
31
|
+
* `npm_config_*`) never need a subprocess.
|
|
32
|
+
*
|
|
33
|
+
* SECURITY BOUNDARIES (flair#1692). This module is where every version
|
|
34
|
+
* decision flows through, so the resolver is the right chokepoint:
|
|
35
|
+
*
|
|
36
|
+
* - SCHEME ALLOWLIST. Only `https:` is fetched by default. `http:` is
|
|
37
|
+
* allowed ONLY for a loopback host (127.0.0.1 / localhost / ::1) — the CI
|
|
38
|
+
* lane's registry — or when the operator explicitly opts in with
|
|
39
|
+
* `FLAIR_ALLOW_INSECURE_REGISTRY=1`, in which case the resolution is
|
|
40
|
+
* reported as INSECURE. `file:`/`ftp:`/anything else is refused outright.
|
|
41
|
+
* A refusal names the actor, the state, and the remedy; it is never silent.
|
|
42
|
+
*
|
|
43
|
+
* - SEMVER VALIDATION. A registry may return any string as `latest`, and npm
|
|
44
|
+
* accepts `pkg@https://attacker/x.tgz` as a remote-tarball install spec.
|
|
45
|
+
* So a fetched version is validated as strict semver BEFORE it can be used
|
|
46
|
+
* as an install spec (see `isStrictSemver` / `fetchLatestVersion`). A
|
|
47
|
+
* non-semver value is refused and printed, never installed.
|
|
48
|
+
*
|
|
49
|
+
* - REDIRECTS. The version fetch passes `redirect: "error"`: a 302 from an
|
|
50
|
+
* allowed registry cannot silently land on a disallowed host, which would
|
|
51
|
+
* otherwise bypass the scheme allowlist.
|
|
52
|
+
*
|
|
53
|
+
* - TRANSPORT. A bare `fetch()` cannot honour npm's `strict-ssl` / `cafile`
|
|
54
|
+
* / `_authToken`. When the configured registry is a NON-default mirror
|
|
55
|
+
* (i.e. not the public npmjs default) AND npm has transport config we
|
|
56
|
+
* cannot replicate, the lookup is delegated to
|
|
57
|
+
* `npm view <pkg> version --json --registry <url>`, which gets URL, TLS
|
|
58
|
+
* trust, and auth from npm in one step. This is the reviewer's preferred
|
|
59
|
+
* transport (flair#1692 item 4). For the public default npmjs registry we
|
|
60
|
+
* fetch anonymously — public reads need no trust config — and the residual
|
|
61
|
+
* gap is: a custom CA/auth token set for a registry OTHER than the
|
|
62
|
+
* configured one is not carried. `_authToken` cannot be read back anyway:
|
|
63
|
+
* npm marks auth options "protected" and refuses to print them.
|
|
64
|
+
*
|
|
65
|
+
* DEFAULT. When nothing is configured — and when npm is absent or errors —
|
|
66
|
+
* this returns npm's public default, `https://registry.npmjs.org`. A user with
|
|
67
|
+
* no registry configured must see exactly the behaviour they saw before this
|
|
68
|
+
* module existed.
|
|
69
|
+
*/
|
|
70
|
+
import { execFile } from "node:child_process";
|
|
71
|
+
/** npm's public default, and the pre-flair#1688 behaviour. No trailing slash. */
|
|
72
|
+
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
73
|
+
/**
|
|
74
|
+
* Opt-in for a deliberately plain-http registry that is not loopback. Any
|
|
75
|
+
* other non-https scheme is refused even with this set (flair#1692).
|
|
76
|
+
*/
|
|
77
|
+
export const INSECURE_REGISTRY_ENV = "FLAIR_ALLOW_INSECURE_REGISTRY";
|
|
78
|
+
/**
|
|
79
|
+
* Thrown when the configured registry is not allowed. The message already
|
|
80
|
+
* contains actor + state + remedy, so callers only have to print it.
|
|
81
|
+
*/
|
|
82
|
+
export class RegistryRefusalError extends Error {
|
|
83
|
+
rawUrl;
|
|
84
|
+
source;
|
|
85
|
+
constructor(message, rawUrl, source) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "RegistryRefusalError";
|
|
88
|
+
this.rawUrl = rawUrl;
|
|
89
|
+
this.source = source;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ─── npm config: value + source ─────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* npm represents "unset" as the literal string `undefined` from
|
|
95
|
+
* `npm config get`, and may pad with whitespace. Normalise all of that, plus a
|
|
96
|
+
* trailing slash, to a clean base URL (or null).
|
|
97
|
+
*/
|
|
98
|
+
function normalizeRegistryValue(raw) {
|
|
99
|
+
if (raw == null)
|
|
100
|
+
return null;
|
|
101
|
+
const trimmed = raw.trim();
|
|
102
|
+
if (trimmed === "" || trimmed === "undefined" || trimmed === "null")
|
|
103
|
+
return null;
|
|
104
|
+
return trimmed.replace(/\/+$/, "");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The `@scope` of a package name, or null for unscoped names.
|
|
108
|
+
* `@tpsdev-ai/flair` → `@tpsdev-ai`; `flair` → null.
|
|
109
|
+
*
|
|
110
|
+
* The scope is restricted to npm's legal scope characters. The result is
|
|
111
|
+
* interpolated into an npm config key (and, on Windows, a shell command), so
|
|
112
|
+
* a name that is not a real npm scope must never reach that path.
|
|
113
|
+
*/
|
|
114
|
+
export function packageScope(packageName) {
|
|
115
|
+
if (!packageName.startsWith("@"))
|
|
116
|
+
return null;
|
|
117
|
+
const slash = packageName.indexOf("/");
|
|
118
|
+
if (slash <= 1)
|
|
119
|
+
return null;
|
|
120
|
+
const scope = packageName.slice(0, slash);
|
|
121
|
+
return /^@[A-Za-z0-9._~-]+$/.test(scope) ? scope : null;
|
|
122
|
+
}
|
|
123
|
+
function describeConfigLayer(layer, from) {
|
|
124
|
+
switch (layer) {
|
|
125
|
+
case "env":
|
|
126
|
+
return "env";
|
|
127
|
+
case "cli":
|
|
128
|
+
return "command line";
|
|
129
|
+
case "project":
|
|
130
|
+
return `project .npmrc (${from})`;
|
|
131
|
+
case "user":
|
|
132
|
+
return `user .npmrc (${from})`;
|
|
133
|
+
case "global":
|
|
134
|
+
return `global .npmrc (${from})`;
|
|
135
|
+
case "builtin":
|
|
136
|
+
return "npm builtin";
|
|
137
|
+
default:
|
|
138
|
+
return from || "npm config";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse the human output of `npm config list` into key → {value, source}.
|
|
143
|
+
*
|
|
144
|
+
* Only non-default config is printed, grouped under section headers such as
|
|
145
|
+
* `; "project" config from /path/.npmrc`. Values already overridden by a
|
|
146
|
+
* higher layer are printed COMMENTED OUT (`; registry = … ; overridden by …`),
|
|
147
|
+
* so the single active line per key wins — which is exactly npm's precedence.
|
|
148
|
+
* `publishConfig` values are publish-time only and are ignored.
|
|
149
|
+
*
|
|
150
|
+
* Exported for the unit tests that pin the parse without spawning npm.
|
|
151
|
+
*/
|
|
152
|
+
export function parseNpmConfigList(stdout) {
|
|
153
|
+
const entries = new Map();
|
|
154
|
+
let layer = "unknown";
|
|
155
|
+
let source = "npm config";
|
|
156
|
+
let ignore = false;
|
|
157
|
+
for (const rawLine of String(stdout).split(/\r?\n/)) {
|
|
158
|
+
const line = rawLine.trim();
|
|
159
|
+
if (line === "")
|
|
160
|
+
continue;
|
|
161
|
+
const header = line.match(/^;\s*"([^"]+)"(?:\s+config)?\s+from\s+(.+)$/);
|
|
162
|
+
if (header) {
|
|
163
|
+
const name = header[1];
|
|
164
|
+
const from = header[2].trim();
|
|
165
|
+
if (name === "publishConfig") {
|
|
166
|
+
ignore = true;
|
|
167
|
+
layer = "publish";
|
|
168
|
+
source = from;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
ignore = false;
|
|
172
|
+
const knownLayers = ["env", "cli", "project", "user", "global", "builtin"];
|
|
173
|
+
layer = knownLayers.includes(name)
|
|
174
|
+
? name
|
|
175
|
+
: "unknown";
|
|
176
|
+
source = describeConfigLayer(layer, from);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Comments (including overridden values) and the trailing node-version
|
|
180
|
+
// footer are not config we can use.
|
|
181
|
+
if (line.startsWith(";"))
|
|
182
|
+
continue;
|
|
183
|
+
if (ignore)
|
|
184
|
+
continue;
|
|
185
|
+
const m = line.match(/^([^=]+?)\s*=\s*(.*)$/);
|
|
186
|
+
if (!m)
|
|
187
|
+
continue;
|
|
188
|
+
const key = m[1].trim();
|
|
189
|
+
let value = m[2].trim();
|
|
190
|
+
// npm redacts auth in `npm config list`; presence is all we need.
|
|
191
|
+
if (value === "(protected)")
|
|
192
|
+
value = "";
|
|
193
|
+
else if (value.startsWith('"') && value.endsWith('"')) {
|
|
194
|
+
try {
|
|
195
|
+
value = JSON.parse(value);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
value = value.slice(1, -1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
entries.set(key, { value, layer, source });
|
|
202
|
+
}
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
// ─── Default readers: ask npm, memoised per process ─────────────────────────
|
|
206
|
+
let entriesCache = null;
|
|
207
|
+
function runNpmConfigList() {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
execFile(process.platform === "win32" ? "npm.cmd" : "npm", ["config", "list"], { timeout: 5000, encoding: "utf-8", shell: process.platform === "win32" }, (err, stdout) => {
|
|
210
|
+
// npm missing, timed out, or errored — treat as "cannot determine".
|
|
211
|
+
// The caller falls back to the public default, never a broken URL.
|
|
212
|
+
if (err)
|
|
213
|
+
return resolve(new Map());
|
|
214
|
+
try {
|
|
215
|
+
resolve(parseNpmConfigList(String(stdout)));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
resolve(new Map());
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Every explicitly-set npm config value, resolved once per process (the config
|
|
225
|
+
* does not change under a running command).
|
|
226
|
+
*/
|
|
227
|
+
export function defaultNpmConfigEntries() {
|
|
228
|
+
if (!entriesCache)
|
|
229
|
+
entriesCache = runNpmConfigList();
|
|
230
|
+
return entriesCache;
|
|
231
|
+
}
|
|
232
|
+
/** Default `NpmConfigEntryReader` — one key from the memoised config map. */
|
|
233
|
+
export async function defaultNpmConfigEntryReader(key) {
|
|
234
|
+
const entries = await defaultNpmConfigEntries();
|
|
235
|
+
return entries.get(key) ?? null;
|
|
236
|
+
}
|
|
237
|
+
/** Drop the memoised npm answers — tests that change registry env between runs. */
|
|
238
|
+
export function clearNpmRegistryCache() {
|
|
239
|
+
entriesCache = null;
|
|
240
|
+
}
|
|
241
|
+
// ─── Scheme allowlist ───────────────────────────────────────────────────────
|
|
242
|
+
/** The loopback hosts `http:` is allowed for without an opt-in (CI lane). */
|
|
243
|
+
export function isLoopbackHost(hostname) {
|
|
244
|
+
const h = hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
245
|
+
return h === "127.0.0.1" || h === "localhost" || h === "::1";
|
|
246
|
+
}
|
|
247
|
+
function insecureOptedIn(env) {
|
|
248
|
+
const raw = (env[INSECURE_REGISTRY_ENV] ?? "").trim().toLowerCase();
|
|
249
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
250
|
+
}
|
|
251
|
+
function buildRegistryRefusal(rawUrl, source, problem, scheme) {
|
|
252
|
+
const actor = `the npm registry is configured as ${JSON.stringify(rawUrl)} (source: ${source})`;
|
|
253
|
+
const state = scheme === "http:"
|
|
254
|
+
? "Flair only queries registries over https; plain http is allowed only for a loopback host (127.0.0.1, localhost, ::1)."
|
|
255
|
+
: `Flair refuses the "${scheme ?? "unknown"}" scheme for a registry — only http(s) is considered at all.`;
|
|
256
|
+
const remedy = scheme === "http:"
|
|
257
|
+
? `Point \`registry\` / \`@scope:registry\` at an https URL, or set ${INSECURE_REGISTRY_ENV}=1 to allow this plain-http registry deliberately (file:/ftp: are never allowed).`
|
|
258
|
+
: "Point `registry` / `@scope:registry` at an https URL.";
|
|
259
|
+
return `Refusing npm registry: ${problem}.\n actor: ${actor}\n state: ${state}\n remedy: ${remedy}`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validate a normalized registry URL against the scheme allowlist.
|
|
263
|
+
*
|
|
264
|
+
* Returns the resolution on success (with `insecure` set when the explicit
|
|
265
|
+
* opt-in permitted a non-loopback `http:` registry) and throws a
|
|
266
|
+
* `RegistryRefusalError` whose message carries actor + state + remedy.
|
|
267
|
+
*/
|
|
268
|
+
export function validateRegistryUrl(rawUrl, source, env = process.env) {
|
|
269
|
+
let parsed;
|
|
270
|
+
try {
|
|
271
|
+
parsed = new URL(rawUrl);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
throw new RegistryRefusalError(buildRegistryRefusal(rawUrl, source, "it is not a valid URL", null), rawUrl, source);
|
|
275
|
+
}
|
|
276
|
+
const scheme = parsed.protocol.toLowerCase();
|
|
277
|
+
if (scheme === "https:") {
|
|
278
|
+
return { url: rawUrl, source, insecure: false };
|
|
279
|
+
}
|
|
280
|
+
if (scheme === "http:") {
|
|
281
|
+
if (isLoopbackHost(parsed.hostname)) {
|
|
282
|
+
return { url: rawUrl, source, insecure: false };
|
|
283
|
+
}
|
|
284
|
+
if (insecureOptedIn(env)) {
|
|
285
|
+
return { url: rawUrl, source, insecure: true };
|
|
286
|
+
}
|
|
287
|
+
throw new RegistryRefusalError(buildRegistryRefusal(rawUrl, source, "plain http is not allowed for a non-loopback host", scheme), rawUrl, source);
|
|
288
|
+
}
|
|
289
|
+
throw new RegistryRefusalError(buildRegistryRefusal(rawUrl, source, `the "${scheme}" scheme is never fetched`, scheme), rawUrl, source);
|
|
290
|
+
}
|
|
291
|
+
async function readEntry(readConfig, key) {
|
|
292
|
+
try {
|
|
293
|
+
const entry = await readConfig(key);
|
|
294
|
+
if (!entry)
|
|
295
|
+
return null;
|
|
296
|
+
const value = normalizeRegistryValue(entry.value);
|
|
297
|
+
if (!value)
|
|
298
|
+
return null;
|
|
299
|
+
return { ...entry, value };
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// A custom reader must never break version resolution — treat a throw as
|
|
303
|
+
// "unset" and let the fallback (public default) apply.
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Resolve the registry for `packageName` together with HOW it was chosen, and
|
|
309
|
+
* validate its scheme. Throws `RegistryRefusalError` for a disallowed scheme.
|
|
310
|
+
*/
|
|
311
|
+
export async function resolveNpmRegistryDetailed(packageName, deps = {}) {
|
|
312
|
+
const env = deps.env ?? process.env;
|
|
313
|
+
const readConfig = deps.readConfig ?? defaultNpmConfigEntryReader;
|
|
314
|
+
// Scope mapping first: for a scoped package a configured `@scope:registry`
|
|
315
|
+
// beats the default `registry`, and npm resolves project/user/global files.
|
|
316
|
+
const scope = packageScope(packageName);
|
|
317
|
+
if (scope) {
|
|
318
|
+
const envScoped = normalizeRegistryValue(env[`npm_config_${scope}:registry`]);
|
|
319
|
+
if (envScoped) {
|
|
320
|
+
return validateRegistryUrl(envScoped, `env npm_config_${scope}:registry`, env);
|
|
321
|
+
}
|
|
322
|
+
const scoped = await readEntry(readConfig, `${scope}:registry`);
|
|
323
|
+
if (scoped) {
|
|
324
|
+
return validateRegistryUrl(scoped.value, `${scope}:registry (${scoped.source})`, env);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// env `npm_config_registry` beats any .npmrc default, so read it before npm.
|
|
328
|
+
const envDefault = normalizeRegistryValue(env.npm_config_registry);
|
|
329
|
+
if (envDefault) {
|
|
330
|
+
return validateRegistryUrl(envDefault, "env npm_config_registry", env);
|
|
331
|
+
}
|
|
332
|
+
const configured = await readEntry(readConfig, "registry");
|
|
333
|
+
if (configured) {
|
|
334
|
+
return validateRegistryUrl(configured.value, `registry (${configured.source})`, env);
|
|
335
|
+
}
|
|
336
|
+
return validateRegistryUrl(DEFAULT_NPM_REGISTRY, "default npm public registry", env);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Resolve the registry base URL for `packageName` the way npm would, without
|
|
340
|
+
* the source metadata. Throws `RegistryRefusalError` for a disallowed scheme.
|
|
341
|
+
*/
|
|
342
|
+
export async function resolveNpmRegistry(packageName, deps = {}) {
|
|
343
|
+
return (await resolveNpmRegistryDetailed(packageName, deps)).url;
|
|
344
|
+
}
|
|
345
|
+
/** One operator-facing line naming the registry and where it came from. */
|
|
346
|
+
export function formatRegistryLine(res) {
|
|
347
|
+
const flag = res.insecure ? " [INSECURE]" : "";
|
|
348
|
+
return `registry: ${res.url} (source: ${res.source})${flag}`;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* A printer that emits each distinct registry line once per process, so a
|
|
352
|
+
* listing that resolves the same registry for N packages prints one line.
|
|
353
|
+
*/
|
|
354
|
+
export function createRegistryNoticePrinter(sink = (line) => console.log(line)) {
|
|
355
|
+
const seen = new Set();
|
|
356
|
+
return (res) => {
|
|
357
|
+
const line = formatRegistryLine(res);
|
|
358
|
+
if (seen.has(line))
|
|
359
|
+
return;
|
|
360
|
+
seen.add(line);
|
|
361
|
+
sink(line);
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Resolve + validate for a caller that only wants to REPORT the registry
|
|
366
|
+
* (status/doctor). Never throws: a refusal comes back as `error`.
|
|
367
|
+
*/
|
|
368
|
+
export async function resolveRegistryNotice(packageName, deps = {}) {
|
|
369
|
+
try {
|
|
370
|
+
const res = await resolveNpmRegistryDetailed(packageName, deps);
|
|
371
|
+
return { line: formatRegistryLine(res), error: null };
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
if (err instanceof RegistryRefusalError)
|
|
375
|
+
return { line: null, error: err.message };
|
|
376
|
+
return { line: null, error: err instanceof Error ? err.message : String(err) };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ─── Strict semver ──────────────────────────────────────────────────────────
|
|
380
|
+
/**
|
|
381
|
+
* The semver.org regex, exact. A registry value must match this before it can
|
|
382
|
+
* be used as an `npm install` spec: npm accepts `pkg@<url>` as a remote-tarball
|
|
383
|
+
* spec, so a hostile/compromised registry returning a URL as `latest` would
|
|
384
|
+
* otherwise turn the update check into an arbitrary install (flair#1692).
|
|
385
|
+
*/
|
|
386
|
+
const STRICT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
387
|
+
/** True when `value` is a strict semver version (not a range, tag, or URL). */
|
|
388
|
+
export function isStrictSemver(value) {
|
|
389
|
+
return typeof value === "string" && STRICT_SEMVER.test(value.trim());
|
|
390
|
+
}
|
|
391
|
+
function boolValue(value) {
|
|
392
|
+
return value == null ? true : !/^(false|0|no)$/i.test(value.trim());
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The npm auth-config key that applies to `registryUrl`, or null. npm stores a
|
|
396
|
+
* registry token as `//host/path/:_authToken` (longest matching prefix wins),
|
|
397
|
+
* plus a legacy unscoped `_authToken`. npm refuses to print the VALUE; we only
|
|
398
|
+
* need to know one exists so we can delegate to `npm view` (flair#1692 item 4).
|
|
399
|
+
*/
|
|
400
|
+
export function registryAuthTokenKey(registryUrl, entries) {
|
|
401
|
+
if (entries.has("_authToken"))
|
|
402
|
+
return "_authToken";
|
|
403
|
+
let parsed;
|
|
404
|
+
try {
|
|
405
|
+
parsed = new URL(registryUrl);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const hostPath = `//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}/`;
|
|
411
|
+
let best = null;
|
|
412
|
+
for (const key of entries.keys()) {
|
|
413
|
+
if (!key.endsWith(":_authToken"))
|
|
414
|
+
continue;
|
|
415
|
+
const prefix = key.slice(0, -":_authToken".length);
|
|
416
|
+
if (!prefix.endsWith("/"))
|
|
417
|
+
continue;
|
|
418
|
+
if (!hostPath.startsWith(prefix) && !prefix.startsWith(hostPath))
|
|
419
|
+
continue;
|
|
420
|
+
if (best === null || prefix.length > best.length - ":_authToken".length)
|
|
421
|
+
best = key;
|
|
422
|
+
}
|
|
423
|
+
return best;
|
|
424
|
+
}
|
|
425
|
+
/** Read npm's transport trust config (TLS + auth) for the fetch decision. */
|
|
426
|
+
export async function readRegistryTransport(registryUrl, deps = {}) {
|
|
427
|
+
const readConfig = deps.readConfig ?? defaultNpmConfigEntryReader;
|
|
428
|
+
const readMap = deps.readConfigMap ?? defaultNpmConfigEntries;
|
|
429
|
+
const strictSsl = boolValue((await readEntry(readConfig, "strict-ssl"))?.value);
|
|
430
|
+
const cafile = (await readEntry(readConfig, "cafile"))?.value ?? null;
|
|
431
|
+
const ca = (await readEntry(readConfig, "ca"))?.value ?? null;
|
|
432
|
+
let authTokenKey = null;
|
|
433
|
+
try {
|
|
434
|
+
authTokenKey = registryAuthTokenKey(registryUrl, await readMap());
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
authTokenKey = null;
|
|
438
|
+
}
|
|
439
|
+
return { strictSsl, cafile, ca, authTokenKey };
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* True when the transport config cannot be honoured by a bare `fetch()` and
|
|
443
|
+
* the lookup must go through npm (`npm view`). Covers a custom CA, a disabled
|
|
444
|
+
* TLS check, and a configured registry auth token.
|
|
445
|
+
*/
|
|
446
|
+
export function registryNeedsNpmTransport(config) {
|
|
447
|
+
return !config.strictSsl || config.cafile != null || config.ca != null || config.authTokenKey != null;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* GET a registry URL as JSON with redirects REFUSED. `redirect: "error"` is
|
|
451
|
+
* load-bearing: without it a 302 from an allow-listed registry would silently
|
|
452
|
+
* land on a disallowed host (flair#1692 item 5).
|
|
453
|
+
*/
|
|
454
|
+
async function fetchRegistryJson(url, timeoutMs, fetchImpl) {
|
|
455
|
+
const doFetch = fetchImpl ?? fetch;
|
|
456
|
+
try {
|
|
457
|
+
const res = await doFetch(url, {
|
|
458
|
+
redirect: "error",
|
|
459
|
+
headers: { accept: "application/json" },
|
|
460
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
461
|
+
});
|
|
462
|
+
if (!res.ok)
|
|
463
|
+
return { ok: false, message: `registry returned HTTP ${res.status}` };
|
|
464
|
+
return { ok: true, data: await res.json(), message: "" };
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
const message = err instanceof Error && err.message ? err.message : "registry fetch failed";
|
|
468
|
+
return { ok: false, message };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function npmBin() {
|
|
472
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* `npm view <spec> <field> --json`. Used when npm transport config (CA, TLS
|
|
476
|
+
* override, auth token) cannot be replicated by `fetch()`.
|
|
477
|
+
*/
|
|
478
|
+
function runNpmViewJson(spec, field, registryUrl, timeoutMs) {
|
|
479
|
+
return new Promise((resolve) => {
|
|
480
|
+
execFile(npmBin(), ["view", spec, field, "--json", "--registry", registryUrl], { timeout: timeoutMs, encoding: "utf-8", shell: process.platform === "win32" }, (err, stdout, stderr) => {
|
|
481
|
+
if (err) {
|
|
482
|
+
const message = String(stderr || err.message || "npm view failed").trim();
|
|
483
|
+
return resolve({ ok: false, message });
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
resolve({ ok: true, data: JSON.parse(String(stdout).trim()), message: "" });
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
resolve({ ok: false, message: "npm view returned non-JSON output" });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** True when this registry should be queried through npm rather than fetch(). */
|
|
495
|
+
async function useNpmTransport(registry, deps) {
|
|
496
|
+
if (registry.url === DEFAULT_NPM_REGISTRY)
|
|
497
|
+
return false;
|
|
498
|
+
const transport = await readRegistryTransport(registry.url, deps);
|
|
499
|
+
return registryNeedsNpmTransport(transport);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Resolve the registry for `packageName` and fetch its `latest` dist-tag,
|
|
503
|
+
* validating the result as strict semver before returning it. Never throws for
|
|
504
|
+
* a refusal or a network failure — those come back as discriminated results so
|
|
505
|
+
* every caller can decide whether to skip, warn, or abort.
|
|
506
|
+
*/
|
|
507
|
+
export async function fetchLatestVersion(packageName, deps = {}) {
|
|
508
|
+
let registry;
|
|
509
|
+
try {
|
|
510
|
+
registry = await resolveNpmRegistryDetailed(packageName, deps);
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
if (err instanceof RegistryRefusalError)
|
|
514
|
+
return { kind: "refused", message: err.message };
|
|
515
|
+
return { kind: "refused", message: err instanceof Error ? err.message : String(err) };
|
|
516
|
+
}
|
|
517
|
+
deps.onRegistry?.(registry);
|
|
518
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
519
|
+
let raw = null;
|
|
520
|
+
if (await useNpmTransport(registry, deps)) {
|
|
521
|
+
const out = await runNpmViewJson(packageName, "version", registry.url, timeoutMs);
|
|
522
|
+
if (!out.ok)
|
|
523
|
+
return { kind: "unavailable", message: out.message, registry };
|
|
524
|
+
if (typeof out.data === "string")
|
|
525
|
+
raw = out.data;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const out = await fetchRegistryJson(`${registry.url}/${packageName}/latest`, timeoutMs, deps.fetchImpl);
|
|
529
|
+
if (!out.ok)
|
|
530
|
+
return { kind: "unavailable", message: out.message, registry };
|
|
531
|
+
const data = out.data;
|
|
532
|
+
if (data && typeof data.version === "string")
|
|
533
|
+
raw = data.version;
|
|
534
|
+
}
|
|
535
|
+
if (raw == null || raw.trim() === "") {
|
|
536
|
+
return { kind: "unavailable", message: "registry response had no version", registry };
|
|
537
|
+
}
|
|
538
|
+
const value = raw.trim();
|
|
539
|
+
if (!isStrictSemver(value))
|
|
540
|
+
return { kind: "invalid", value, registry };
|
|
541
|
+
return { kind: "ok", version: value, registry };
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Resolve the registry for `packageName` and fetch the declared `dependencies`
|
|
545
|
+
* map for `version`. Used to decide the Harper engine version a target flair
|
|
546
|
+
* release declares, so the same registry/auth/TLS path is used.
|
|
547
|
+
*/
|
|
548
|
+
export async function fetchDeclaredDependencies(packageName, version, deps = {}) {
|
|
549
|
+
let registry;
|
|
550
|
+
try {
|
|
551
|
+
registry = await resolveNpmRegistryDetailed(packageName, deps);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
if (err instanceof RegistryRefusalError)
|
|
555
|
+
return { kind: "refused", message: err.message };
|
|
556
|
+
return { kind: "refused", message: err instanceof Error ? err.message : String(err) };
|
|
557
|
+
}
|
|
558
|
+
deps.onRegistry?.(registry);
|
|
559
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
560
|
+
let data;
|
|
561
|
+
if (await useNpmTransport(registry, deps)) {
|
|
562
|
+
const out = await runNpmViewJson(`${packageName}@${version}`, "dependencies", registry.url, timeoutMs);
|
|
563
|
+
if (!out.ok)
|
|
564
|
+
return { kind: "unavailable", message: out.message, registry };
|
|
565
|
+
data = out.data;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const out = await fetchRegistryJson(`${registry.url}/${packageName}/${version}`, timeoutMs, deps.fetchImpl);
|
|
569
|
+
if (!out.ok)
|
|
570
|
+
return { kind: "unavailable", message: out.message, registry };
|
|
571
|
+
data = out.data;
|
|
572
|
+
}
|
|
573
|
+
const depsField = data?.dependencies;
|
|
574
|
+
const dependencies = depsField && typeof depsField === "object" && !Array.isArray(depsField)
|
|
575
|
+
? depsField
|
|
576
|
+
: null;
|
|
577
|
+
return { kind: "ok", dependencies, registry };
|
|
578
|
+
}
|
|
@@ -26,8 +26,13 @@
|
|
|
26
26
|
* the resolved agent, never from the tool arguments — an agent can only act as
|
|
27
27
|
* itself (no forging of agentId / authorId in the body).
|
|
28
28
|
*
|
|
29
|
-
* NOTE (flair#1580): MCP-facing metadata (name,
|
|
30
|
-
* output shape) lives in
|
|
29
|
+
* NOTE (flair#1580, vendored by flair#1683): MCP-facing metadata (name,
|
|
30
|
+
* description, inputSchema, output shape) lives in the descriptor module
|
|
31
|
+
* vendored at build time into `./tool-descriptors/` (source of truth:
|
|
32
|
+
* packages/flair-tool-descriptors/src/index.ts — see
|
|
33
|
+
* scripts/vendor-tool-descriptors.mjs). It is imported by RELATIVE PATH so it
|
|
34
|
+
* resolves inside the packed tarball with no registry dependency on the
|
|
35
|
+
* private descriptor package. This registry
|
|
31
36
|
* binds each *native* descriptor to its Harper impl. The stdio adapter binds
|
|
32
37
|
* the same list (stdio surface) to FlairClient HTTP — tool-set drift is
|
|
33
38
|
* impossible by construction. One-sided tools (`attention`, archive verbs,
|
|
@@ -53,7 +58,7 @@ import { resolveVersion } from "./version.js";
|
|
|
53
58
|
import { agentContext, adminContext, collectionResource } from "./in-process.js";
|
|
54
59
|
import { unionUsageMemoryIds } from "./usage-ids.js";
|
|
55
60
|
import { SKILL_TAG, isSkillWrite } from "./skill-write.js";
|
|
56
|
-
import { NATIVE_TOOL_DESCRIPTORS, toMcpToolDef, } from "
|
|
61
|
+
import { NATIVE_TOOL_DESCRIPTORS, toMcpToolDef, } from "./tool-descriptors/index.js";
|
|
57
62
|
const H = {};
|
|
58
63
|
const LOADERS = {
|
|
59
64
|
SemanticSearch: async () => (await import("./SemanticSearch.js")).SemanticSearch,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT, DO NOT COMMIT (flair#1683).
|
|
2
|
+
// Verbatim build-time copy of packages/flair-tool-descriptors/src/index.ts, the single source of truth.
|
|
3
|
+
// Regenerated by scripts/vendor-tool-descriptors.mjs (see each package's
|
|
4
|
+
// `prebuild`). Edit the source, not this copy.
|
|
1
5
|
/**
|
|
2
6
|
* Transport-agnostic MCP tool descriptors (flair#1580).
|
|
3
7
|
*
|
package/dist/version-check.js
CHANGED
|
@@ -32,6 +32,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
33
|
import { dirname, join } from "node:path";
|
|
34
34
|
import { parseSemverCore } from "./fabric-upgrade.js";
|
|
35
|
+
import { fetchLatestVersion } from "./lib/npm-registry.js";
|
|
35
36
|
export const FLAIR_PKG_NAME = "@tpsdev-ai/flair";
|
|
36
37
|
export const DEFAULT_CACHE_PATH = join(homedir(), ".flair", ".version-check-cache.json");
|
|
37
38
|
/** How long a cached "latest" answer is trusted before we re-hit the registry. */
|
|
@@ -65,19 +66,39 @@ function writeCacheFile(path, entry) {
|
|
|
65
66
|
}
|
|
66
67
|
async function defaultFetchLatest(timeoutMs) {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// flair#1688: honour the registry npm is configured to use (scoped
|
|
70
|
+
// `@scope:registry`, project/user/global .npmrc, env `npm_config_registry`)
|
|
71
|
+
// rather than a hardcoded public host — a private mirror must not be
|
|
72
|
+
// silently bypassed by the update check.
|
|
73
|
+
//
|
|
74
|
+
// flair#1692: `fetchLatestVersion` validates the URL scheme, refuses
|
|
75
|
+
// redirects, applies npm's transport where a bare fetch cannot, and —
|
|
76
|
+
// critically — validates the returned value as strict semver before it can
|
|
77
|
+
// ever be used as an install spec.
|
|
78
|
+
const result = await fetchLatestVersion(FLAIR_PKG_NAME, { timeoutMs });
|
|
79
|
+
if (result.kind === "ok")
|
|
80
|
+
return result.version;
|
|
81
|
+
if (result.kind === "invalid") {
|
|
82
|
+
// A hostile/compromised registry can return a URL or tag as `latest`;
|
|
83
|
+
// npm would install `pkg@<url>` as a remote tarball. Refuse, and say so
|
|
84
|
+
// rather than silently treating it as "no update".
|
|
85
|
+
console.error(`flair version check: the configured registry returned a non-semver "latest" (${JSON.stringify(result.value)}) ` +
|
|
86
|
+
`from ${result.registry.url} — refusing to use it. Check \`registry\` / \`@scope:registry\` (source: ${result.registry.source}).`);
|
|
72
87
|
return null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
}
|
|
89
|
+
if (result.kind === "refused") {
|
|
90
|
+
console.error(result.message);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
77
93
|
// Offline, DNS failure, timeout, registry 5xx, bad JSON — all the same:
|
|
78
94
|
// we couldn't determine "latest" over the network this time.
|
|
79
95
|
return null;
|
|
80
96
|
}
|
|
97
|
+
catch {
|
|
98
|
+
// Defense-in-depth: the helper already swallows its failure modes. This
|
|
99
|
+
// guards the status/doctor never-throws contract even if it does not.
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
81
102
|
}
|
|
82
103
|
export function defaultVersionCheckDeps() {
|
|
83
104
|
return {
|
package/docs/releasing.md
CHANGED
|
@@ -8,8 +8,7 @@ A maintainer then approves the staged tarballs on npmjs.com with 2FA to make the
|
|
|
8
8
|
> `flair-bench` is version-bumped and tagged in lockstep with the other 7, and stages in
|
|
9
9
|
> its own step in CI for [historical reasons](#flair-bench-bootstrap-one-time-done). That
|
|
10
10
|
> step is no longer allowed to fail: every already-published package must stage
|
|
11
|
-
> for a release to pass.
|
|
12
|
-
> first-publish exception until an npm org owner bootstraps it.
|
|
11
|
+
> for a release to pass.
|
|
13
12
|
|
|
14
13
|
```
|
|
15
14
|
merge release PR ──▶ push tag v0.11.0 ──▶ CI stages all packages ──▶ npm staging
|
|
@@ -147,17 +146,15 @@ Leave `npm publish` **unchecked** under allowed actions. This structurally preve
|
|
|
147
146
|
CI/OIDC identity from publishing anything live directly — the only path to live is the
|
|
148
147
|
human 2FA approval of a staged package.
|
|
149
148
|
|
|
150
|
-
Packages: `flair-client`, `flair-
|
|
149
|
+
Packages: `flair-client`, `flair-mcp`, `flair`,
|
|
151
150
|
`openclaw-flair`, `pi-flair`, `n8n-nodes-flair`, `langgraph-flair`, `flair-bench`.
|
|
152
151
|
|
|
153
|
-
>
|
|
154
|
-
>
|
|
155
|
-
>
|
|
156
|
-
>
|
|
157
|
-
>
|
|
158
|
-
>
|
|
159
|
-
> `scripts/materialize-bundled-descriptors.mjs`) so `npm install` of those
|
|
160
|
-
> tarballs does not 404.
|
|
152
|
+
> `flair-tool-descriptors` is private and never published. Since flair#1683 it is a
|
|
153
|
+
> **build-time source**: `scripts/vendor-tool-descriptors.mjs` copies it into each
|
|
154
|
+
> consumer's own tree at prebuild (`resources/tool-descriptors/` for `flair`,
|
|
155
|
+
> `packages/flair-mcp/src/tool-descriptors/` for `flair-mcp`) and the consumers
|
|
156
|
+
> import it by relative path. Nothing declares or bundles it as a dependency —
|
|
157
|
+
> 0.54.1's `bundleDependencies` broke fresh global installs (flair#1681 → #1683).
|
|
161
158
|
|
|
162
159
|
### `flair-bench` bootstrap (one-time, done)
|
|
163
160
|
|
|
@@ -196,6 +193,15 @@ review. Because the release is triggered by a tag push, its deployment policy mu
|
|
|
196
193
|
**`v*` tags** (Settings → Environments → `release` → Deployment branches and tags →
|
|
197
194
|
Selected branches and tags → add tag rule `v*`).
|
|
198
195
|
|
|
196
|
+
### Required status checks (ruleset)
|
|
197
|
+
|
|
198
|
+
Add **`First-publish preflight`** to the main branch ruleset's required status
|
|
199
|
+
checks (repo settings → Rules → main). This is a repo-settings act, not a code
|
|
200
|
+
change: until it is listed, the release-PR job is **advisory only** and a red
|
|
201
|
+
first-publish preflight can be merged past. The tag-triggered
|
|
202
|
+
`release-publish.yml` also runs the same check before staging, so a hazard is
|
|
203
|
+
still stopped on the normal release path either way.
|
|
204
|
+
|
|
199
205
|
### Approver 2FA
|
|
200
206
|
|
|
201
207
|
The maintainer who approves staged packages must have 2FA enabled on their npm account.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.2",
|
|
4
4
|
"packageManager": "bun@1.3.10",
|
|
5
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
6
6
|
"type": "module",
|
|
@@ -46,11 +46,10 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
48
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
49
|
-
"prebuild": "npm run clean &&
|
|
49
|
+
"prebuild": "npm run clean && node scripts/vendor-tool-descriptors.mjs",
|
|
50
50
|
"build": "tsc -p tsconfig.json --noCheck && node scripts/write-build-info.mjs",
|
|
51
51
|
"build:cli": "tsc -p tsconfig.cli.json --noCheck && node scripts/write-build-info.mjs",
|
|
52
|
-
"prepack": "
|
|
53
|
-
"prepublishOnly": "npm run build && npm run build:cli",
|
|
52
|
+
"prepack": "npm run build && npm run build:cli",
|
|
54
53
|
"test": "bun run test:unit",
|
|
55
54
|
"test:unit": "bun scripts/test-unit.ts",
|
|
56
55
|
"test:e2e": "playwright test",
|
|
@@ -72,12 +71,8 @@
|
|
|
72
71
|
"jose": "6.2.2",
|
|
73
72
|
"js-yaml": "^4.3.2",
|
|
74
73
|
"tar": "^7.5.22",
|
|
75
|
-
"tweetnacl": "1.0.3"
|
|
76
|
-
"@tpsdev-ai/flair-tool-descriptors": "0.54.1"
|
|
74
|
+
"tweetnacl": "1.0.3"
|
|
77
75
|
},
|
|
78
|
-
"bundleDependencies": [
|
|
79
|
-
"@tpsdev-ai/flair-tool-descriptors"
|
|
80
|
-
],
|
|
81
76
|
"overrides": {
|
|
82
77
|
"react-native-fs": "npm:empty-npm-package@1.0.0",
|
|
83
78
|
"brace-expansion": "^5.0.9",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Apache License
|
|
2
|
-
Version 2.0, January 2004
|
|
3
|
-
http://www.apache.org/licenses/
|
|
4
|
-
|
|
5
|
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
-
|
|
7
|
-
Copyright 2026 TPS Dev AI
|
|
8
|
-
|
|
9
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
-
you may not use this file except in compliance with the License.
|
|
11
|
-
You may obtain a copy of the License at
|
|
12
|
-
|
|
13
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
-
|
|
15
|
-
Unless required by applicable law or agreed to in writing, software
|
|
16
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
-
See the License for the specific language governing permissions and
|
|
19
|
-
limitations under the License.
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# @tpsdev-ai/flair-tool-descriptors
|
|
2
|
-
|
|
3
|
-
Transport-agnostic MCP tool descriptors for [Flair](https://tps.dev/#flair).
|
|
4
|
-
|
|
5
|
-
This package is **pure data and types**: tool name, description, JSON Schema
|
|
6
|
-
`inputSchema`, output shape, and reviewed surface flags. It imports neither
|
|
7
|
-
Harper nor FlairClient. The Flair server binds each native descriptor to its
|
|
8
|
-
Harper implementation; `@tpsdev-ai/flair-mcp` binds each stdio descriptor to a
|
|
9
|
-
FlairClient HTTP call. Both tool sets are derived from this list, so a new
|
|
10
|
-
descriptor appears on every listed surface with zero hand-wiring (flair#1580).
|
|
11
|
-
|
|
12
|
-
## Install
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install @tpsdev-ai/flair-tool-descriptors
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Surfaces
|
|
19
|
-
|
|
20
|
-
`native` and `stdio` default to true. Set either to `false` for a reviewed
|
|
21
|
-
one-sided tool (`attention` is native-only; `relationship_store` is
|
|
22
|
-
stdio-only). The #1578 exemption list is derived from those flags.
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport-agnostic MCP tool descriptors (flair#1580).
|
|
3
|
-
*
|
|
4
|
-
* Pure data + types: name, description, inputSchema, output shape, and
|
|
5
|
-
* reviewed surface flags. No Harper, no FlairClient, no Zod, no HTTP.
|
|
6
|
-
*
|
|
7
|
-
* The server TOOLS registry binds each native descriptor to its Harper impl.
|
|
8
|
-
* The flair-mcp stdio adapter binds each stdio descriptor to a FlairClient
|
|
9
|
-
* call. Both tool sets are DERIVED from this list — a new descriptor appears
|
|
10
|
-
* on every surface that lists it, with zero hand-wiring.
|
|
11
|
-
*/
|
|
12
|
-
/** JSON Schema object used as MCP tools/list inputSchema. */
|
|
13
|
-
export interface JsonSchemaObject {
|
|
14
|
-
type: "object";
|
|
15
|
-
properties: Record<string, JsonSchemaProperty>;
|
|
16
|
-
required?: string[];
|
|
17
|
-
}
|
|
18
|
-
export interface JsonSchemaProperty {
|
|
19
|
-
type?: string;
|
|
20
|
-
description?: string;
|
|
21
|
-
enum?: string[];
|
|
22
|
-
items?: {
|
|
23
|
-
type?: string;
|
|
24
|
-
};
|
|
25
|
-
default?: unknown;
|
|
26
|
-
}
|
|
27
|
-
/** MCP tool descriptor as returned by tools/list. */
|
|
28
|
-
export interface McpToolDef {
|
|
29
|
-
name: string;
|
|
30
|
-
description: string;
|
|
31
|
-
inputSchema: JsonSchemaObject;
|
|
32
|
-
annotations?: Record<string, unknown>;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* One MCP-facing tool. `native` / `stdio` default true — omit both and the
|
|
36
|
-
* tool appears on every surface. Set false for a reviewed one-sided tool
|
|
37
|
-
* (the #1578 exemption list is derived from these flags).
|
|
38
|
-
*/
|
|
39
|
-
export interface ToolDescriptor {
|
|
40
|
-
name: string;
|
|
41
|
-
description: string;
|
|
42
|
-
inputSchema: JsonSchemaObject;
|
|
43
|
-
/** One-line output shape (MCP metadata). Native conformance contracts pin this as `summary`. */
|
|
44
|
-
outputShape: string;
|
|
45
|
-
annotations?: Record<string, unknown>;
|
|
46
|
-
/** When false, native /mcp does not bind this tool. Default true. */
|
|
47
|
-
native?: boolean;
|
|
48
|
-
/** When false, the stdio adapter does not bind this tool. Default true. */
|
|
49
|
-
stdio?: boolean;
|
|
50
|
-
/** Stdio-only description when the HTTP path differs from native /mcp policy. */
|
|
51
|
-
stdioDescription?: string;
|
|
52
|
-
/** Properties advertised on native /mcp only (reviewed, e.g. flair#1579). */
|
|
53
|
-
stdioOmitProperties?: readonly string[];
|
|
54
|
-
/** Properties advertised on the stdio adapter only (reviewed). */
|
|
55
|
-
stdioExtraProperties?: Record<string, JsonSchemaProperty>;
|
|
56
|
-
}
|
|
57
|
-
export declare function isNativeTool(d: ToolDescriptor): boolean;
|
|
58
|
-
export declare function isStdioTool(d: ToolDescriptor): boolean;
|
|
59
|
-
export declare function toMcpToolDef(d: ToolDescriptor): McpToolDef;
|
|
60
|
-
/** Native tools/list def, minus reviewed stdio-only omissions. */
|
|
61
|
-
export declare function toStdioMcpToolDef(d: ToolDescriptor): McpToolDef;
|
|
62
|
-
export declare function descriptorNames(descriptors: readonly ToolDescriptor[]): string[];
|
|
63
|
-
export declare const TOOL_DESCRIPTORS: readonly ToolDescriptor[];
|
|
64
|
-
export declare const NATIVE_TOOL_DESCRIPTORS: readonly ToolDescriptor[];
|
|
65
|
-
export declare const STDIO_TOOL_DESCRIPTORS: readonly ToolDescriptor[];
|
|
66
|
-
/** Derived #1578 exemption list — one-sided by construction, not hand-synced. */
|
|
67
|
-
export declare const SURFACE_EXEMPTIONS: {
|
|
68
|
-
readonly registryOnly: string[];
|
|
69
|
-
readonly adapterOnly: string[];
|
|
70
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tpsdev-ai/flair-tool-descriptors",
|
|
3
|
-
"version": "0.54.1",
|
|
4
|
-
"description": "Transport-agnostic MCP tool descriptors for Flair — name, description, inputSchema, output shape. No Harper, no FlairClient.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist/",
|
|
16
|
-
"LICENSE",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc --noCheck",
|
|
21
|
-
"test": "bun test",
|
|
22
|
-
"prepublishOnly": "npm run build"
|
|
23
|
-
},
|
|
24
|
-
"publishConfig": {
|
|
25
|
-
"access": "public"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18"
|
|
29
|
-
},
|
|
30
|
-
"license": "Apache-2.0",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/tpsdev-ai/flair.git",
|
|
34
|
-
"directory": "packages/flair-tool-descriptors"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://tps.dev/#flair",
|
|
37
|
-
"keywords": [
|
|
38
|
-
"flair",
|
|
39
|
-
"mcp",
|
|
40
|
-
"tools",
|
|
41
|
-
"descriptors"
|
|
42
|
-
],
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"typescript": "5.9.3"
|
|
45
|
-
}
|
|
46
|
-
}
|