@tpsdev-ai/flair 0.10.0 → 0.10.1
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/README.md +3 -3
- package/dist/cli.js +85 -30
- package/dist/resources/AgentCard.js +5 -14
- package/dist/resources/agentcard-fields.js +40 -0
- package/dist/resources/auth-middleware.js +28 -26
- package/dist/resources/health.js +2 -2
- package/package.json +11 -2
- package/schemas/schema.graphql +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Flair is the **identity + memory substrate for AI agents**. Cryptographic per-ag
|
|
|
17
17
|
│ Codex CLI ─┼─[ flair-mcp ]─┐ │
|
|
18
18
|
│ Gemini CLI ─┤ │ │
|
|
19
19
|
│ Continue.dev ─┤ │ ┌──────────────────────┐ │
|
|
20
|
-
│ Goose ─┘ ├─▶ │ Flair (
|
|
20
|
+
│ Goose ─┘ ├─▶ │ Flair (self-hosted) │ │
|
|
21
21
|
│ LangGraph ─[ langgraph-flair ]──│ Ed25519 / HNSW / │ │
|
|
22
22
|
│ OpenClaw ─[ openclaw-flair ]──│ Soul + Memory │ │
|
|
23
23
|
│ n8n ─[ n8n-nodes-flair ]──└──────────┬───────────┘ │
|
|
@@ -146,7 +146,7 @@ Not all memories are equal:
|
|
|
146
146
|
Memories can be time-bounded with `validFrom` and `validTo` fields. Expired memories are excluded from search and bootstrap automatically — no manual cleanup.
|
|
147
147
|
|
|
148
148
|
### Relationship Graph
|
|
149
|
-
Entity-to-entity triples with temporal bounds. Model structured knowledge — "
|
|
149
|
+
Entity-to-entity triples with temporal bounds. Model structured knowledge — "Alice works-with Bob since 2024-01-01" — queryable alongside semantic memory.
|
|
150
150
|
|
|
151
151
|
### Real-Time Feeds
|
|
152
152
|
Subscribe to memory or soul changes via WebSocket/SSE. Useful for dashboards, cross-agent sync, or audit trails.
|
|
@@ -392,7 +392,7 @@ Good for teams with multiple machines or always-on agents.
|
|
|
392
392
|
|
|
393
393
|
### Harper Fabric
|
|
394
394
|
|
|
395
|
-
Deploy Flair on [Harper Fabric](https://www.harperdb.io/) for managed hosting with multi-region replication and failover. Federation
|
|
395
|
+
Deploy Flair on [Harper Fabric](https://www.harperdb.io/) for managed hosting with multi-region replication and failover. Federation runs against Harper Fabric hubs (e.g. `flair.your-org.harperfabric.com`) — pair your local instance to sync memories across nodes.
|
|
396
396
|
|
|
397
397
|
## Security
|
|
398
398
|
|
package/dist/cli.js
CHANGED
|
@@ -3215,28 +3215,45 @@ federation
|
|
|
3215
3215
|
}
|
|
3216
3216
|
const result = await res.json();
|
|
3217
3217
|
console.log(`✅ Paired with hub: ${result.instance?.id ?? hubUrl}`);
|
|
3218
|
-
// Record the hub as our peer
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3218
|
+
// Record the hub as our local peer. This is REQUIRED, not optional:
|
|
3219
|
+
// `flair federation sync` reads the Peer table to find the hub, so
|
|
3220
|
+
// without this record sync reports "No hub peer configured" and silently
|
|
3221
|
+
// never runs. Previously this was gated on `if (adminPass)` and the write
|
|
3222
|
+
// result was never checked — pairing with only an agent key (or a failed
|
|
3223
|
+
// upsert) left no peer behind a misleadingly green "✅ Paired".
|
|
3224
|
+
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD ?? "";
|
|
3225
|
+
if (!adminPass) {
|
|
3226
|
+
console.error("Error: paired on the hub, but the local hub-peer record needs admin auth to write — " +
|
|
3227
|
+
"pass --admin-pass, or set FLAIR_ADMIN_PASS / HDB_ADMIN_PASSWORD, then re-run pair. " +
|
|
3228
|
+
"Without it, 'flair federation sync' will report 'No hub peer configured'.");
|
|
3229
|
+
process.exit(1);
|
|
3230
|
+
}
|
|
3231
|
+
const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
|
|
3232
|
+
const opsEndpoint = resolveEffectiveOpsUrl(opts) ?? `http://127.0.0.1:${resolveOpsPort(opts)}`;
|
|
3233
|
+
const peerRes = await fetch(`${opsEndpoint}/`, {
|
|
3234
|
+
method: "POST",
|
|
3235
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
3236
|
+
body: JSON.stringify({
|
|
3237
|
+
operation: "upsert", database: "flair", table: "Peer",
|
|
3238
|
+
records: [{
|
|
3239
|
+
id: result.instance?.id ?? "hub",
|
|
3240
|
+
publicKey: result.instance?.publicKey ?? "",
|
|
3241
|
+
role: "hub", endpoint: hubUrl, status: "paired",
|
|
3242
|
+
pairedAt: new Date().toISOString(),
|
|
3243
|
+
createdAt: new Date().toISOString(),
|
|
3244
|
+
updatedAt: new Date().toISOString(),
|
|
3245
|
+
}],
|
|
3246
|
+
}),
|
|
3247
|
+
signal: AbortSignal.timeout(10_000),
|
|
3248
|
+
});
|
|
3249
|
+
if (!peerRes.ok) {
|
|
3250
|
+
const text = await peerRes.text().catch(() => "");
|
|
3251
|
+
console.error(`Error: paired with the hub but failed to write the local hub-peer record ` +
|
|
3252
|
+
`(${peerRes.status} ${text.slice(0, 200)}). Ops endpoint: ${opsEndpoint}. ` +
|
|
3253
|
+
`'flair federation sync' will not find the hub until this succeeds — check --admin-pass and the ops port.`);
|
|
3254
|
+
process.exit(1);
|
|
3239
3255
|
}
|
|
3256
|
+
console.log(`✅ Recorded hub as local peer: ${result.instance?.id ?? "hub"} → ${hubUrl}`);
|
|
3240
3257
|
}
|
|
3241
3258
|
catch (err) {
|
|
3242
3259
|
console.error(`Error: ${err.message}`);
|
|
@@ -3373,25 +3390,63 @@ export async function runFederationSyncOnce(opts) {
|
|
|
3373
3390
|
const hubUrl = hub.endpoint ?? hub.id;
|
|
3374
3391
|
// ── Batching constants ──────────────────────────────────────────────
|
|
3375
3392
|
// 2MB JSON budget (server cap is 10MB; 2MB leaves headroom for headers
|
|
3376
|
-
// and signature metadata) +
|
|
3393
|
+
// and signature metadata) + 50 records max per batch. The hub merge itself
|
|
3394
|
+
// is fast (~1.7s/50 records, per its SyncLog), but the Fabric ingress was
|
|
3395
|
+
// observed to intermittently stall on larger POSTs — a 50-record batch hung
|
|
3396
|
+
// ~2 min while the same records split into 2×25 went through immediately.
|
|
3397
|
+
// 50 keeps batches in the reliable range, and sendBatch's adaptive split
|
|
3398
|
+
// recovers if a stretch still stalls.
|
|
3377
3399
|
const BUDGET_BYTES = 2_000_000;
|
|
3378
|
-
const BUDGET_RECORDS =
|
|
3400
|
+
const BUDGET_RECORDS = 50;
|
|
3379
3401
|
// ── sendBatch helper ────────────────────────────────────────────────
|
|
3380
3402
|
// Secret key is lazy-loaded: only needed when there are records to send.
|
|
3381
3403
|
// Loading earlier would cause a spurious error when SQL queries fail
|
|
3382
3404
|
// (e.g. 401) before we know we have records.
|
|
3383
3405
|
let secretKey;
|
|
3406
|
+
// Statuses the Fabric ingress returns when a batch POST didn't complete in
|
|
3407
|
+
// time (408) or was too large (413), plus the transient gateway 5xx family.
|
|
3408
|
+
// Splitting the batch and retrying smaller chunks lets the sync converge
|
|
3409
|
+
// instead of aborting the whole run.
|
|
3410
|
+
const TIMEOUT_STATUSES = new Set([408, 413, 502, 503, 504]);
|
|
3411
|
+
// Per-batch wall-clock cap. Without it a stalled connection to the Fabric
|
|
3412
|
+
// ingress hangs the whole sync until the *gateway's* timeout fires (~2 min
|
|
3413
|
+
// observed), which is what stranded the re-pair. A 45s cap is generous —
|
|
3414
|
+
// a healthy 50-record batch merges in <2s — so a trip means a real stall,
|
|
3415
|
+
// and we split-and-retry rather than wait it out.
|
|
3416
|
+
const BATCH_TIMEOUT_MS = 45_000;
|
|
3384
3417
|
async function sendBatch(batch) {
|
|
3385
3418
|
if (!secretKey)
|
|
3386
3419
|
secretKey = await loadInstanceSecretKey(instance.id, opts);
|
|
3387
3420
|
const syncBody = { instanceId: instance.id, records: batch, lamportClock: Date.now() };
|
|
3388
3421
|
const signedSyncBody = signBodyFresh(syncBody, secretKey);
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3422
|
+
// Halve and retry down to a single record. Covers both an explicit
|
|
3423
|
+
// timeout status AND a client-side abort (stalled socket). The hub
|
|
3424
|
+
// merges idempotently (put-by-id), so retried records are safe.
|
|
3425
|
+
const splittable = (status) => batch.length > 1 && (status === null || TIMEOUT_STATUSES.has(status));
|
|
3426
|
+
const split = async () => {
|
|
3427
|
+
const mid = Math.floor(batch.length / 2);
|
|
3428
|
+
const left = await sendBatch(batch.slice(0, mid));
|
|
3429
|
+
const right = await sendBatch(batch.slice(mid));
|
|
3430
|
+
return { merged: left.merged + right.merged, skipped: left.skipped + right.skipped };
|
|
3431
|
+
};
|
|
3432
|
+
let syncRes;
|
|
3433
|
+
try {
|
|
3434
|
+
syncRes = await fetch(`${hubUrl}/FederationSync`, {
|
|
3435
|
+
method: "POST",
|
|
3436
|
+
headers: { "Content-Type": "application/json" },
|
|
3437
|
+
body: JSON.stringify(signedSyncBody),
|
|
3438
|
+
signal: AbortSignal.timeout(BATCH_TIMEOUT_MS),
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
catch (err) {
|
|
3442
|
+
// Timeout/abort or network drop — no status. Split if we can.
|
|
3443
|
+
if (splittable(null))
|
|
3444
|
+
return await split();
|
|
3445
|
+
throw new Error(`Sync batch (${batch.length} record${batch.length === 1 ? "" : "s"}) failed: ${err?.message ?? err}`);
|
|
3446
|
+
}
|
|
3394
3447
|
if (!syncRes.ok) {
|
|
3448
|
+
if (splittable(syncRes.status))
|
|
3449
|
+
return await split();
|
|
3395
3450
|
const text = await syncRes.text().catch(() => "");
|
|
3396
3451
|
throw new Error(`Sync batch failed: ${syncRes.status} ${text}`);
|
|
3397
3452
|
}
|
|
@@ -4693,7 +4748,7 @@ function oauthDetailLines(o) {
|
|
|
4693
4748
|
// discoverLocalFlairPort when the configured URL is unreachable, to detect
|
|
4694
4749
|
// config-vs-daemon port drift (ops-mbdi). Order is ad-hoc — first hit wins.
|
|
4695
4750
|
//
|
|
4696
|
-
// 9926: original default (long-running
|
|
4751
|
+
// 9926: original default (long-running early installs predate the bump)
|
|
4697
4752
|
// 19926: current default (DEFAULT_PORT)
|
|
4698
4753
|
// 19925: ops-anvil VM secondary
|
|
4699
4754
|
const LOCAL_FLAIR_PROBE_PORTS = [9926, 19926, 19925];
|
|
@@ -6969,7 +7024,7 @@ memory.command("list")
|
|
|
6969
7024
|
});
|
|
6970
7025
|
// ─── flair memory hygiene ────────────────────────────────────────────────────
|
|
6971
7026
|
// Detect + remove junk memory rows that accumulate over time. Surfaced from
|
|
6972
|
-
// the 2026-05-07 manual cleanup (ops-ucyy):
|
|
7027
|
+
// the 2026-05-07 manual cleanup (ops-ucyy): an instance had 627 records, ~250 of
|
|
6973
7028
|
// them were noise — `*-compact-*` ID fragments from an old pipeline, pangram
|
|
6974
7029
|
// test content ("the quick brown fox..." / "Flair 251 test ..."), and
|
|
6975
7030
|
// near-empty rows (<25 chars). We did the cleanup ad-hoc with raw curl + jq;
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
|
|
3
|
-
return String(entry.kind ?? entry.key ?? "").trim().toLowerCase();
|
|
4
|
-
}
|
|
5
|
-
function readSoulContent(entry) {
|
|
6
|
-
return String(entry.content ?? entry.value ?? "").trim();
|
|
7
|
-
}
|
|
2
|
+
import { selectPublicDescription, selectPublicSkills } from "./agentcard-fields.js";
|
|
8
3
|
export class AgentCard extends Resource {
|
|
9
4
|
async get(pathInfo) {
|
|
10
5
|
const agentId = (typeof pathInfo === "string" ? pathInfo : null) ??
|
|
@@ -25,19 +20,15 @@ export class AgentCard extends Resource {
|
|
|
25
20
|
if (row?.agentId === agentId)
|
|
26
21
|
souls.push(row);
|
|
27
22
|
}
|
|
28
|
-
const descriptionEntry = souls.find((s) => readSoulKind(s) === "description" && readSoulContent(s)) ??
|
|
29
|
-
souls.find((s) => readSoulContent(s));
|
|
30
|
-
const skills = souls
|
|
31
|
-
.filter((s) => readSoulKind(s) === "capability")
|
|
32
|
-
.map((s) => readSoulContent(s))
|
|
33
|
-
.filter(Boolean);
|
|
34
23
|
return {
|
|
35
24
|
name: String(agent.name ?? agent.id ?? agentId),
|
|
36
|
-
description
|
|
25
|
+
// Only an explicit kind="description" soul publishes — no private-soul
|
|
26
|
+
// fallback (ops-vz6j). See agentcard-fields.ts for the security rationale.
|
|
27
|
+
description: selectPublicDescription(souls),
|
|
37
28
|
url: String(agent.url ?? ""),
|
|
38
29
|
version: String(agent.version ?? "1.0.0"),
|
|
39
30
|
capabilities: agent.capabilities && typeof agent.capabilities === "object" ? agent.capabilities : {},
|
|
40
|
-
skills,
|
|
31
|
+
skills: selectPublicSkills(souls),
|
|
41
32
|
defaultInputModes: ["text"],
|
|
42
33
|
defaultOutputModes: ["text"],
|
|
43
34
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Pure selection logic for what an agent's public AgentCard exposes.
|
|
2
|
+
//
|
|
3
|
+
// SECURITY: `GET /AgentCard/{id}` is intentionally UNAUTHENTICATED (A2A spec —
|
|
4
|
+
// agent cards are public discovery metadata, and the auth-middleware allow-list
|
|
5
|
+
// lets it bypass auth). Everything these helpers return is therefore world-
|
|
6
|
+
// readable. They must publish ONLY explicitly-kinded souls — never an implicit
|
|
7
|
+
// fallback that could surface a private soul (an operator's internal note,
|
|
8
|
+
// prompt fragment, or credential reminder) on the public endpoint.
|
|
9
|
+
//
|
|
10
|
+
// Kept free of any @harperfast/harper import so the real logic is unit-testable
|
|
11
|
+
// without spinning up Harper (avoids the simulator-pattern that let the
|
|
12
|
+
// description-fallback leak ship untested — ops-vz6j).
|
|
13
|
+
export function readSoulKind(entry) {
|
|
14
|
+
return String(entry.kind ?? entry.key ?? "").trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
export function readSoulContent(entry) {
|
|
17
|
+
return String(entry.content ?? entry.value ?? "").trim();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The public description is ONLY an explicit `kind="description"` soul.
|
|
21
|
+
*
|
|
22
|
+
* There is deliberately no fallback to "the first soul with any content": that
|
|
23
|
+
* was the ops-vz6j leak — an agent with no description soul would publish an
|
|
24
|
+
* arbitrary private soul on the unauthenticated card. Absent an explicit
|
|
25
|
+
* description, publish nothing.
|
|
26
|
+
*/
|
|
27
|
+
export function selectPublicDescription(souls) {
|
|
28
|
+
const entry = souls.find((s) => readSoulKind(s) === "description" && readSoulContent(s));
|
|
29
|
+
return entry ? readSoulContent(entry) : "";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Public skills are ONLY `kind="capability"` souls — an explicit, operator-
|
|
33
|
+
* intended allow-list. Any other soul kind is never published.
|
|
34
|
+
*/
|
|
35
|
+
export function selectPublicSkills(souls) {
|
|
36
|
+
return souls
|
|
37
|
+
.filter((s) => readSoulKind(s) === "capability")
|
|
38
|
+
.map((s) => readSoulContent(s))
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
@@ -142,7 +142,7 @@ server.http(async (request, nextLayer) => {
|
|
|
142
142
|
// and inline JS, with no embedded data. The JS prompts for admin-pass and
|
|
143
143
|
// auths every API call (/Agent, /SemanticSearch, /FederationPeers, etc).
|
|
144
144
|
// Without this allow-list entry, the HTML is 401-blocked on hosted Flair
|
|
145
|
-
// instances (
|
|
145
|
+
// instances (a localhost caller works only because authorizeLocal=true).
|
|
146
146
|
url.pathname === "/ObservationCenter")
|
|
147
147
|
return nextLayer(request);
|
|
148
148
|
// If Harper has already authorized this request (e.g. authorizeLocal=true on localhost),
|
|
@@ -272,32 +272,34 @@ server.http(async (request, nextLayer) => {
|
|
|
272
272
|
request.tpsAgent = agentId;
|
|
273
273
|
request._tpsAuthVerified = true;
|
|
274
274
|
request.tpsAgentIsAdmin = await isAdmin(agentId);
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
275
|
+
// Grant Harper-level permissions for the cryptographically-verified agent by
|
|
276
|
+
// setting request.user directly to the admin super_user.
|
|
277
|
+
//
|
|
278
|
+
// Prior approach swapped the Authorization header to "Basic admin:<pass>" so
|
|
279
|
+
// Harper's auth pipeline would re-authenticate with full (HNSW-capable)
|
|
280
|
+
// permissions. That silently broke under Harper 5.0.9: its authentication
|
|
281
|
+
// layer (security/auth.ts `authentication()`) reads the Authorization header
|
|
282
|
+
// and resolves request.user BEFORE invoking this middleware via nextHandler.
|
|
283
|
+
// A TPS-Ed25519 header matches no Basic/Bearer strategy, so Harper sets
|
|
284
|
+
// request.user = null and never re-reads the header — our post-hoc swap was
|
|
285
|
+
// ignored, the request ran unauthenticated, and Harper surfaced it downstream
|
|
286
|
+
// as a generic "Login failed" 401. (Broke at the 2026-05-27 daemon restart,
|
|
287
|
+
// which loaded 5.0.9 fresh; the prior long-running process held an older
|
|
288
|
+
// in-memory Harper where the late header read still worked.)
|
|
289
|
+
//
|
|
290
|
+
// Setting request.user directly is the supported extension path and is honored
|
|
291
|
+
// by all downstream resources, including HNSW vector search. getUser(admin,
|
|
292
|
+
// null) looks up the admin record WITHOUT password validation — safe here
|
|
293
|
+
// because the Ed25519 signature verified above already proves agent identity
|
|
294
|
+
// cryptographically; no Basic credential is presented. This preserves the
|
|
295
|
+
// prior privilege model (verified agents act with admin perms; per-agent data
|
|
296
|
+
// isolation is still enforced downstream via the x-tps-agent header below).
|
|
297
|
+
try {
|
|
298
|
+
request.user = await server.getUser("admin", null, request);
|
|
294
299
|
}
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
request.user = await server.getUser("admin", null, request);
|
|
299
|
-
}
|
|
300
|
-
catch { }
|
|
300
|
+
catch {
|
|
301
|
+
// Admin record unavailable — request proceeds as the verified tpsAgent
|
|
302
|
+
// without elevated perms; resource-level scoping still applies.
|
|
301
303
|
}
|
|
302
304
|
// Propagate authenticated agent to downstream resources via header.
|
|
303
305
|
// Resources can read this to enforce agent-level scoping.
|
package/dist/resources/health.js
CHANGED
|
@@ -23,10 +23,10 @@ const exists = async (path) => {
|
|
|
23
23
|
* which is what makes /Health work for callers outside `authorizeLocal`'s
|
|
24
24
|
* localhost-bypass. Without this, Harper's intrinsic Basic-auth gate fires
|
|
25
25
|
* BEFORE our HTTP middleware can apply the /Health bypass list in
|
|
26
|
-
* auth-middleware.ts, so remote callers (e.g., from
|
|
26
|
+
* auth-middleware.ts, so remote callers (e.g., from a remote host hitting a Fabric-
|
|
27
27
|
* hosted Flair) get a 401 even though the Resource handler is intentionally
|
|
28
28
|
* unauthenticated. Symptom matched: Fabric returned 401 + WWW-Authenticate:
|
|
29
|
-
* Basic on /Health while
|
|
29
|
+
* Basic on /Health while a localhost caller returned 200 — the difference was
|
|
30
30
|
* Harper's localhost-auto-auth, not anything in our code.
|
|
31
31
|
*
|
|
32
32
|
* Same pattern as `FederationPair.allowCreate(){ return true }` (PR #299):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"node": ">=22"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@harperfast/harper": "5.0.
|
|
58
|
+
"@harperfast/harper": "5.0.21",
|
|
59
59
|
"@types/js-yaml": "^4.0.9",
|
|
60
60
|
"commander": "14.0.3",
|
|
61
61
|
"harper-fabric-embeddings": "0.2.3",
|
|
@@ -64,6 +64,15 @@
|
|
|
64
64
|
"tar": "^7.5.13",
|
|
65
65
|
"tweetnacl": "1.0.3"
|
|
66
66
|
},
|
|
67
|
+
"optionalDependencies": {
|
|
68
|
+
"@node-llama-cpp/linux-arm64": "3.18.1",
|
|
69
|
+
"@node-llama-cpp/linux-armv7l": "3.18.1",
|
|
70
|
+
"@node-llama-cpp/linux-x64": "3.18.1",
|
|
71
|
+
"@node-llama-cpp/mac-arm64-metal": "3.18.1",
|
|
72
|
+
"@node-llama-cpp/mac-x64": "3.18.1",
|
|
73
|
+
"@node-llama-cpp/win-arm64": "3.18.1",
|
|
74
|
+
"@node-llama-cpp/win-x64": "3.18.1"
|
|
75
|
+
},
|
|
67
76
|
"devDependencies": {
|
|
68
77
|
"@playwright/test": "^1.59.1",
|
|
69
78
|
"@types/node": "24.11.0",
|
package/schemas/schema.graphql
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ─── Observatory tables ───────────────────────────────────────────────────────
|
|
3
3
|
|
|
4
4
|
type ObsOffice @table(database: "flair") @export {
|
|
5
|
-
id: ID @primaryKey # e.g. "
|
|
5
|
+
id: ID @primaryKey # e.g. "office1"
|
|
6
6
|
name: String!
|
|
7
7
|
publicKey: String! # Ed25519 hex public key for auth verification
|
|
8
8
|
status: String @indexed # "online" | "offline" | "degraded"
|