@tpsdev-ai/flair 0.53.0 → 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.
Files changed (97) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1791 -15648
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1665 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +931 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1628 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/engine-version.js +12 -4
  43. package/dist/fabric-npm-install.js +87 -0
  44. package/dist/fabric-upgrade.js +30 -15
  45. package/dist/federation-verify.js +498 -0
  46. package/dist/fleet-verify.js +144 -21
  47. package/dist/install/clients.js +167 -0
  48. package/dist/lib/auth-resolve.js +76 -1
  49. package/dist/lib/daemon-liveness.js +131 -2
  50. package/dist/lib/doctor-config-path.js +61 -0
  51. package/dist/lib/doctor-federation-driver.js +189 -0
  52. package/dist/lib/doctor-run.js +40 -0
  53. package/dist/lib/entity-vocab-cli.js +3 -3
  54. package/dist/lib/federation-pair-identity.js +47 -0
  55. package/dist/lib/launchd-repair.js +5 -4
  56. package/dist/lib/npm-registry.js +578 -0
  57. package/dist/lib/ops-api-bind.js +115 -0
  58. package/dist/lib/owned-pins.js +219 -0
  59. package/dist/lib/uninstall-purge.js +218 -0
  60. package/dist/rem/restore.js +8 -10
  61. package/dist/resources/AgentReadPosition.js +74 -0
  62. package/dist/resources/Federation.js +8 -2
  63. package/dist/resources/Memory.js +4 -3
  64. package/dist/resources/MemoryBootstrap.js +41 -25
  65. package/dist/resources/MemoryCandidate.js +5 -6
  66. package/dist/resources/OrgEventCatchup.js +126 -47
  67. package/dist/resources/agent-read-position-lib.js +83 -0
  68. package/dist/resources/agent-read-position.js +120 -0
  69. package/dist/resources/embeddings-boot.js +32 -0
  70. package/dist/resources/federation-peer-liveness.js +73 -0
  71. package/dist/resources/health.js +68 -19
  72. package/dist/resources/mcp-tools.js +48 -279
  73. package/dist/resources/memory-visibility.js +3 -3
  74. package/dist/resources/migration-boot.js +59 -18
  75. package/dist/resources/migrations/embedding-stamp.js +20 -1
  76. package/dist/resources/migrations/recheck.js +43 -0
  77. package/dist/resources/migrations/runner.js +6 -1
  78. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  79. package/dist/resources/migrations/visibility-backfill.js +2 -2
  80. package/dist/resources/org-event-catchup-lib.js +47 -0
  81. package/dist/resources/record-owner-guard.js +1 -0
  82. package/dist/resources/tool-descriptors/index.js +669 -0
  83. package/dist/stamp-migration-verify.js +163 -0
  84. package/dist/stamp-outstanding.js +144 -0
  85. package/dist/version-check.js +29 -8
  86. package/docs/api-reference.md +4 -2
  87. package/docs/deploying-on-fabric.md +11 -10
  88. package/docs/deployment.md +3 -1
  89. package/docs/federation.md +19 -0
  90. package/docs/hosted-on-fabric.md +3 -3
  91. package/docs/quickstart.md +2 -1
  92. package/docs/releasing.md +20 -6
  93. package/docs/spoke-bringup.md +10 -5
  94. package/docs/standalone-local.md +3 -1
  95. package/docs/upgrade.md +25 -6
  96. package/package.json +4 -4
  97. package/schemas/agent.graphql +15 -0
@@ -0,0 +1,114 @@
1
+ import { resolveKeyPath, buildEd25519Auth } from "../lib/auth-resolve.js";
2
+ let cli;
3
+ /** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
4
+ export function bindCli(fns) {
5
+ cli = fns;
6
+ }
7
+ function resolveBaseUrl(opts) {
8
+ return cli.resolveBaseUrl(opts);
9
+ }
10
+ function resolveSigningAgentId(opts, command) {
11
+ return cli.resolveSigningAgentId(opts, command);
12
+ }
13
+ function parseEntitiesOptionOrExit(csv) {
14
+ return cli.parseEntitiesOptionOrExit(csv);
15
+ }
16
+ // ─── flair workspace ─────────────────────────────────────────────────────────
17
+ //
18
+ // Coordination write surface (Kris #510). `workspace set` writes the
19
+ // agent's OWN WorkspaceState via a signed PUT /WorkspaceState/{id}. Identity
20
+ // is asserted by including agentId in the body — the server never trusts it
21
+ // blindly, it 403s any mismatch against the Ed25519 signature's agentId
22
+ // (WorkspaceState.put(), resources/WorkspaceState.ts), so this is a
23
+ // self-declaration the server verifies 1:1, not attribution-from-body.
24
+ //
25
+ // (flair#679, measured against a real spawned Harper): table-backed resources
26
+ // only accept writes via PUT /<Table>/<id> — a bare POST /WorkspaceState 405s
27
+ // ("does not have a post method implemented to handle HTTP method POST"),
28
+ // same restriction documented in resources/Memory.ts and already fixed for
29
+ // `soul set` (#498). WorkspaceState.ts DOES define a post() method, but
30
+ // Harper's REST layer never routes a real HTTP POST to it — post() is only
31
+ // reachable via in-process resource instantiation, never the wire. put(),
32
+ // unlike post(), does NOT default createdAt/timestamp/agentId — the CLI
33
+ // supplies them all explicitly below.
34
+ export const MAX_WORKSPACE_FIELD_LENGTH = 2000;
35
+ /** Register the `flair workspace` command group (flair#1635). */
36
+ export function register(program) {
37
+ const ENTITIES_OPTION_DESCRIPTION = cli.ENTITIES_OPTION_DESCRIPTION;
38
+ const workspace = program.command("workspace").description("Manage agent workspace state (The Office Space)");
39
+ workspace
40
+ .command("set")
41
+ .description("Set your agent's current workspace state (PUT /WorkspaceState/{id})")
42
+ .requiredOption("--ref <ref>", "Workspace ref (branch, worktree, or task ref)")
43
+ .option("--label <text>", "Human-readable label for this workspace")
44
+ .option("--provider <name>", "Provider/runtime (e.g. claude-code, openclaw)", "cli")
45
+ .option("--task <id>", "Task/issue id this workspace is attached to")
46
+ .option("--phase <phase>", "Current phase (e.g. design, implement, review)")
47
+ .option("--summary <text>", "Short summary of current workspace state")
48
+ .option("--entities <csv>", ENTITIES_OPTION_DESCRIPTION)
49
+ .option("--agent <id>", "Agent ID (env: FLAIR_AGENT_ID)")
50
+ .option("--port <port>", "Harper HTTP port")
51
+ .option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET)")
52
+ .action(async (opts) => {
53
+ const { agentId } = resolveSigningAgentId(opts, "workspace set");
54
+ if (!agentId) {
55
+ console.error("Error: agent ID required. Pass --agent <id> or set FLAIR_AGENT_ID environment variable.");
56
+ process.exit(1);
57
+ }
58
+ // Validate field lengths (free text → cap to bound the write).
59
+ for (const [name, val] of [["ref", opts.ref], ["label", opts.label], ["summary", opts.summary]]) {
60
+ if (val && String(val).length > MAX_WORKSPACE_FIELD_LENGTH) {
61
+ console.error(`Error: --${name} exceeds ${MAX_WORKSPACE_FIELD_LENGTH} character limit (got ${String(val).length}).`);
62
+ process.exit(1);
63
+ }
64
+ }
65
+ // flair#1288: validate --entities before any key/network work; exits 1
66
+ // with the canonical format-and-type-set message on any malformed value.
67
+ const entities = opts.entities ? parseEntitiesOptionOrExit(String(opts.entities)) : undefined;
68
+ const keyPath = resolveKeyPath(agentId);
69
+ if (!keyPath) {
70
+ console.error(`Error: private key not found for agent '${agentId}'. Check ~/.flair/keys/ or set FLAIR_KEY_DIR.`);
71
+ process.exit(1);
72
+ }
73
+ const baseUrl = resolveBaseUrl(opts).replace(/\/$/, "");
74
+ // Deterministic id (agentId:ref) — re-running `workspace set` for the same
75
+ // ref overwrites the same record, which is intentional (one row per
76
+ // agent+ref, not an append log).
77
+ const id = `${agentId}:${opts.ref}`;
78
+ const auth = buildEd25519Auth(agentId, "PUT", `/WorkspaceState/${id}`, keyPath);
79
+ // agentId IS included in the body now — WorkspaceState.put() (unlike
80
+ // post()) does not auto-attribute from the signature, it 403s any
81
+ // mismatch. This is a self-declaration the server verifies against the
82
+ // signature, not a forgeable claim.
83
+ const now = new Date().toISOString();
84
+ const body = {
85
+ id,
86
+ agentId,
87
+ ref: opts.ref,
88
+ provider: opts.provider ?? "cli",
89
+ timestamp: now,
90
+ createdAt: now,
91
+ };
92
+ if (opts.label)
93
+ body.label = opts.label;
94
+ if (opts.task)
95
+ body.taskId = opts.task;
96
+ if (opts.phase)
97
+ body.phase = opts.phase;
98
+ if (opts.summary)
99
+ body.summary = opts.summary;
100
+ if (entities && entities.length > 0)
101
+ body.entities = entities;
102
+ const res = await fetch(`${baseUrl}/WorkspaceState/${id}`, {
103
+ method: "PUT",
104
+ headers: { "Content-Type": "application/json", Authorization: auth },
105
+ body: JSON.stringify(body),
106
+ });
107
+ if (!res.ok) {
108
+ const text = await res.text().catch(() => "");
109
+ console.error(`Error: PUT /WorkspaceState/${id} failed (${res.status}): ${text}`);
110
+ process.exit(1);
111
+ }
112
+ console.log(`✓ Workspace state updated for '${agentId}': ref=${opts.ref}${opts.phase ? `, phase=${opts.phase}` : ""}`);
113
+ });
114
+ }
package/dist/deploy.js CHANGED
@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { createRequire } from "node:module";
7
7
  import { COMPONENT_ENV_FILENAME, PUBLIC_URL_KEY, isLoopbackUrl, planComponentEnv, publicUrlRemedy, } from "./component-env.js";
8
8
  import { awaitOriginQuiescent, awaitReplicationConvergence, defaultConvergenceDeps, parseReplicationFailure, } from "./replication-convergence.js";
9
+ import { DEFAULT_STAMP_VERIFY_TIMEOUT_MS, verifyStampMigrationConverged } from "./stamp-migration-verify.js";
10
+ import { FABRIC_NPM_INSTALL_COMMAND } from "./fabric-npm-install.js";
9
11
  // Files that must be PRESENT for a deploy root to be usable at all — a
10
12
  // preflight sanity check, not the payload definition.
11
13
  //
@@ -218,6 +220,12 @@ export function buildHarperDeployArgs(opts, url, project) {
218
220
  `replicated=${opts.replicated !== false}`,
219
221
  `deployment_timeout=${deploymentTimeoutMs}`,
220
222
  `install_timeout=${installTimeoutMs}`,
223
+ // flair#886: Harper's default `npm install` writes every tarball into
224
+ // the node's `~/.npm/_cacache` and never evicts it. This command runs
225
+ // the same install against a disposable cache and deletes it after
226
+ // (see src/fabric-npm-install.ts). One argv element so Harper's
227
+ // `key=value` parser keeps the spaces inside the value.
228
+ `install_command=${FABRIC_NPM_INSTALL_COMMAND}`,
221
229
  ];
222
230
  // --ignore-replication-errors escape hatch. Only appended when
223
231
  // set — omitted entirely otherwise, so this is a no-op for every existing
@@ -929,6 +937,22 @@ export async function deploy(opts) {
929
937
  timeoutMs: opts.verifyTimeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS,
930
938
  onProgress: opts.onProgress,
931
939
  });
940
+ // flair#1073: route verify proves the component is serving. It does not
941
+ // prove embedding-stamp re-embedded pre-flip rows. `upgrade --target`
942
+ // was reporting success while a Fabric corpus stayed split for days.
943
+ // Same --no-verify escape hatch; needs Basic admin (token-only skips).
944
+ if (opts.fabricUser && opts.fabricPassword) {
945
+ await verifyStampMigrationConverged({
946
+ baseUrl: url,
947
+ fabricUser: opts.fabricUser,
948
+ fabricPassword: opts.fabricPassword,
949
+ timeoutMs: opts.verifyTimeoutMs ?? DEFAULT_STAMP_VERIFY_TIMEOUT_MS,
950
+ onProgress: opts.onProgress,
951
+ });
952
+ }
953
+ else {
954
+ opts.onProgress?.("skipping embedding-stamp convergence check — needs --fabric-user/--fabric-password (token-only auth cannot read /HealthDetail)");
955
+ }
932
956
  // Only meaningful for a target that is not loopback: a local Harper SHOULD
933
957
  // advertise loopback, and asserting otherwise there would be wrong.
934
958
  if (publicUrl) {
@@ -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
- const res = await fetch(`https://registry.npmjs.org/@tpsdev-ai/flair/${flairVersion}`, { signal: AbortSignal.timeout(5000) });
51
- if (!res.ok)
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
- const data = await res.json();
54
- return data.dependencies?.harper ?? data.dependencies?.["@harperfast/harper"] ?? null;
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;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * fabric-npm-install.ts — node-side `npm install` for Fabric deploys (flair#886).
3
+ *
4
+ * Harper's default component install writes every fetched tarball into the
5
+ * node's `~/.npm/_cacache` and never evicts it. A month of `flair deploy` /
6
+ * `flair upgrade --target` therefore leaves a growing cache on the hub —
7
+ * hundreds of MB per install once `node-llama-cpp` prebuilds are in the
8
+ * tree — while `get_components` and `system_information` never show it.
9
+ *
10
+ * This file is the `install_command` those two CLI paths pass to
11
+ * `harper deploy`. It runs `npm install` against a disposable cache
12
+ * directory and deletes that directory afterwards, so cache lifetime
13
+ * matches the deploy rather than the node.
14
+ *
15
+ * Harper splits `install_command` on spaces and spawns without a shell
16
+ * (`Application.installApplication`), so the command must be a single
17
+ * executable plus a path — no `sh -c`, no `&& rm`. The flags after
18
+ * `install` match Harper's own default when `install.command` is unset
19
+ * (`--force --ignore-scripts`); a custom command replaces that default
20
+ * entirely, and dropping either flag would change what the node runs.
21
+ */
22
+ import { spawnSync } from "node:child_process";
23
+ import { mkdtempSync, rmSync } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join, resolve } from "node:path";
26
+ import { pathToFileURL } from "node:url";
27
+ /** Path Harper runs, relative to the extracted component directory. */
28
+ export const FABRIC_NPM_INSTALL_ENTRY = "dist/fabric-npm-install.js";
29
+ /**
30
+ * Passed as `install_command=…` to `harper deploy`. Harper's
31
+ * `buildRequest` JSON-parses values that look like JSON; this string
32
+ * does not, so it stays a string. `split(" ")` must yield exactly
33
+ * `["node", FABRIC_NPM_INSTALL_ENTRY]`.
34
+ */
35
+ export const FABRIC_NPM_INSTALL_COMMAND = `node ${FABRIC_NPM_INSTALL_ENTRY}`;
36
+ /** Harper's default argv when `install.command` is unset, plus `--cache`. */
37
+ export const FABRIC_NPM_INSTALL_ARGS_BEFORE_CACHE = [
38
+ "install",
39
+ "--force",
40
+ "--ignore-scripts",
41
+ ];
42
+ /**
43
+ * Run `npm install` with `--cache` pointed at a fresh temp directory,
44
+ * then remove that directory on success or failure.
45
+ *
46
+ * Returns npm's exit status. Throws only when the spawn itself fails
47
+ * to start (ENOENT on `npm`, etc.) — the same split `fabric-upgrade`'s
48
+ * local staging install uses.
49
+ */
50
+ export function installWithEphemeralNpmCache(deps = {}) {
51
+ const spawn = deps.spawn ??
52
+ ((command, args, opts) => spawnSync(command, args, opts));
53
+ const mkdtemp = deps.mkdtemp ?? ((prefix) => mkdtempSync(prefix));
54
+ const rm = deps.rm ?? ((path) => rmSync(path, { recursive: true, force: true }));
55
+ const cwd = deps.cwd ?? process.cwd();
56
+ const tmp = deps.tmpdir ?? tmpdir();
57
+ const cacheDir = mkdtemp(join(tmp, "flair-npm-"));
58
+ try {
59
+ const result = spawn("npm", [...FABRIC_NPM_INSTALL_ARGS_BEFORE_CACHE, "--cache", cacheDir], { cwd, stdio: "inherit" });
60
+ if (result.error)
61
+ throw result.error;
62
+ return result.status ?? 1;
63
+ }
64
+ finally {
65
+ rm(cacheDir);
66
+ }
67
+ }
68
+ function invokedAsMain() {
69
+ const entry = process.argv[1];
70
+ if (!entry)
71
+ return false;
72
+ try {
73
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ if (invokedAsMain()) {
80
+ try {
81
+ process.exit(installWithEphemeralNpmCache());
82
+ }
83
+ catch (err) {
84
+ console.error(err instanceof Error ? err.message : err);
85
+ process.exit(1);
86
+ }
87
+ }
@@ -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
- const REGISTRY = "https://registry.npmjs.org";
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
- const res = await fetch(`${REGISTRY}/${FLAIR_PKG}/latest`, {
160
- signal: AbortSignal.timeout(10_000),
161
- });
162
- if (!res.ok) {
163
- throw new Error(`npm registry returned ${res.status} for ${FLAIR_PKG}/latest`);
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
- const data = (await res.json());
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 res = await fetch(`${REGISTRY}/${FLAIR_PKG}/${flairVersion}`, {
172
- signal: AbortSignal.timeout(10_000),
182
+ const result = await fetchDeclaredDependencies(FLAIR_PKG, flairVersion, {
183
+ timeoutMs: 10_000,
184
+ onRegistry: noticeRegistry,
173
185
  });
174
- if (!res.ok)
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 = data.dependencies?.[name];
195
+ const declared = result.dependencies?.[name];
181
196
  if (declared)
182
197
  return declared;
183
198
  }