@tpsdev-ai/flair 0.44.11 → 0.44.13
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 +1 -1
- package/dist/resources/Memory.js +25 -0
- package/dist/resources/MemoryBootstrap.js +75 -43
- package/dist/resources/SemanticSearch.js +7 -10
- package/dist/resources/memory-durability.js +40 -0
- package/docs/deploying-on-fabric.md +3 -0
- package/docs/deployment-shapes.md +1 -1
- package/docs/deployment.md +5 -4
- package/docs/hosted-on-fabric.md +2 -0
- package/docs/integrations.md +2 -1
- package/docs/quickstart-fabric.md +106 -0
- package/docs/quickstart.md +2 -0
- package/docs/standalone-local.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -347,7 +347,7 @@ flair agent add otherbot --target https://your-server:19926
|
|
|
347
347
|
|
|
348
348
|
### Harper Fabric
|
|
349
349
|
|
|
350
|
-
Managed hosting with multi-region replication and failover.
|
|
350
|
+
Managed hosting with multi-region replication and failover. Need a public URL for Cursor / Grok Bot / cloud agents? Start at **[docs/quickstart-fabric.md](docs/quickstart-fabric.md)**. Federation, pairing, and operator detail: **[docs/deploying-on-fabric.md](docs/deploying-on-fabric.md)**.
|
|
351
351
|
|
|
352
352
|
## Security
|
|
353
353
|
|
package/dist/resources/Memory.js
CHANGED
|
@@ -7,6 +7,7 @@ import { scanFields, isStrictMode } from "./content-safety.js";
|
|
|
7
7
|
import { invalidEntitiesResponse } from "./entity-vocab.js";
|
|
8
8
|
import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
9
9
|
import { assertValidVisibility } from "./memory-visibility.js";
|
|
10
|
+
import { assertValidDurability } from "./memory-durability.js";
|
|
10
11
|
import { DEDUP_COSINE_THRESHOLD_DEFAULT, DEDUP_LEXICAL_THRESHOLD_DEFAULT, DEDUP_MIN_CONTENT_LENGTH, computeMatchConfidence, cosineSimilarity, isConservativeMatch, } from "./dedup.js";
|
|
11
12
|
import { buildProvenance, makeAuthGate, makeReadScope, makeByIdReadGate, makeScopedSearch, resolveAuthGate, stampAttribution, FORBIDDEN, UNAUTH, } from "./record-type-kit.js";
|
|
12
13
|
import { RECORD_TYPES } from "./record-types.js";
|
|
@@ -599,6 +600,19 @@ export class Memory extends databases.flair.Memory {
|
|
|
599
600
|
const usedMemoryIds = content?.usedMemoryIds;
|
|
600
601
|
if (content && typeof content === "object")
|
|
601
602
|
delete content.usedMemoryIds;
|
|
603
|
+
// ── flair#1238: refuse an unrecognised durability BEFORE defaulting ──
|
|
604
|
+
// defaultVisibilityForDurability treats any non-permanent/persistent string
|
|
605
|
+
// as the private branch, so an unknown durability via raw REST (or a future
|
|
606
|
+
// non-Python adapter) is silently accepted and lands on the narrower private
|
|
607
|
+
// branch by accident — fail-safe, but unvalidated by contract. Refusing at
|
|
608
|
+
// the schema boundary makes it safe by construction (mirrors the visibility
|
|
609
|
+
// guard below). Absent durability is accepted and defaulted to "standard".
|
|
610
|
+
{
|
|
611
|
+
const durabilityError = assertValidDurability(content.durability);
|
|
612
|
+
if (durabilityError) {
|
|
613
|
+
return new Response(JSON.stringify({ error: "invalid_durability", message: durabilityError }), { status: 400, headers: { "content-type": "application/json" } });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
602
616
|
content.durability ||= "standard";
|
|
603
617
|
content.createdAt = new Date().toISOString();
|
|
604
618
|
content.updatedAt = content.createdAt;
|
|
@@ -789,6 +803,17 @@ export class Memory extends databases.flair.Memory {
|
|
|
789
803
|
const usedMemoryIds = content?.usedMemoryIds;
|
|
790
804
|
if (content && typeof content === "object")
|
|
791
805
|
delete content.usedMemoryIds;
|
|
806
|
+
// ── flair#1238: refuse an unrecognised durability (mirrors post()) ──
|
|
807
|
+
// put() is the other HTTP-reachable write path (fresh create via CLI, and
|
|
808
|
+
// the update/patch path). Same guard as post(): a present-but-unknown
|
|
809
|
+
// durability is refused with 400; absent is accepted (no default stamped
|
|
810
|
+
// here — put() leaves durability untouched for updates).
|
|
811
|
+
{
|
|
812
|
+
const durabilityError = assertValidDurability(content.durability);
|
|
813
|
+
if (durabilityError) {
|
|
814
|
+
return new Response(JSON.stringify({ error: "invalid_durability", message: durabilityError }), { status: 400, headers: { "content-type": "application/json" } });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
792
817
|
const now = new Date().toISOString();
|
|
793
818
|
content.updatedAt = now;
|
|
794
819
|
// Set defaults that post() sets — put() is also used for new records via CLI
|
|
@@ -142,6 +142,35 @@ const MAX_COLLISION_ENTRIES = 10;
|
|
|
142
142
|
// null` (no relevance score to band), which is CORRECT (Kern's #1220 ruling),
|
|
143
143
|
// never a scoring failure — see the `matchQualityNote` in the response tail.
|
|
144
144
|
const LIFECYCLE_SECTIONS = new Set(["permanent", "recent", "predicted"]);
|
|
145
|
+
// flair#1199 trust-admission — build the EXACT trust entry that ships for one
|
|
146
|
+
// included memory (id + section + block + the conditional matchQualityNote), so
|
|
147
|
+
// the admission loop can charge its REAL serialized cost at the same moment it
|
|
148
|
+
// charges the content cost. Single source of truth: the response tail reuses
|
|
149
|
+
// this same builder, so the charged size and the shipped size can never drift
|
|
150
|
+
// (the #1226 "charge what ships" principle, extended to the per-item trust
|
|
151
|
+
// block). The block is assembled purely for SIZING + the response — its content
|
|
152
|
+
// never enters any authority/scope/attribution/dedup decision (the #735/#744
|
|
153
|
+
// zero-authority invariant).
|
|
154
|
+
//
|
|
155
|
+
// flair#1225 — Kern ruled (on #1220) that a null `matchQuality` on an own-recent
|
|
156
|
+
// (lifecycle) entry is CORRECT: lifecycle sections (permanent/recent/predicted)
|
|
157
|
+
// are a window LOAD, not a retrieval surface, so there is no similarity to band
|
|
158
|
+
// — the null means "not scored here", never a scoring failure on the caller's
|
|
159
|
+
// own records (the #1201 misread: own-recent null beside a teammate band).
|
|
160
|
+
// Behavior is unchanged (per Kern); the matchQualityNote only makes the null
|
|
161
|
+
// self-describing in the payload — Fix 3's "any absent field says why" — so a
|
|
162
|
+
// connector reads it right without knowing the #1201 contract.
|
|
163
|
+
function buildTrustEntry(m, section) {
|
|
164
|
+
const block = buildTrustBlock(m);
|
|
165
|
+
const matchQualityNote = block.matchQuality === null
|
|
166
|
+
? (LIFECYCLE_SECTIONS.has(section)
|
|
167
|
+
? `matchQuality is null because '${section}' is a lifecycle-window section, not a retrieval `
|
|
168
|
+
+ `surface — there is no relevance score to band. This is correct (per flair#1225), not a scoring failure.`
|
|
169
|
+
: "matchQuality is null because no semantic similarity was attached to this result "
|
|
170
|
+
+ "(e.g. a by-id read or a keyword-only degraded match).")
|
|
171
|
+
: undefined;
|
|
172
|
+
return { id: m.id, section, ...block, ...(matchQualityNote ? { matchQualityNote } : {}) };
|
|
173
|
+
}
|
|
145
174
|
// flair#1199/#1206 — the default cap on how many org events bootstrap ships.
|
|
146
175
|
// Overridable per-request via `maxEvents`. Event slots are scarce AND (as of
|
|
147
176
|
// #1199) token-charged, so this bounds both the count and the spend; the shared
|
|
@@ -670,12 +699,17 @@ export class BootstrapMemories extends Resource {
|
|
|
670
699
|
// on the REST path); see contentCost. #1207 stays honored: on the prose
|
|
671
700
|
// path this is still the prose-line cost, so REST recall is unchanged.
|
|
672
701
|
const cost = contentCost(struct, line);
|
|
673
|
-
|
|
702
|
+
// flair#1199 trust-admission — charge the trust block's real serialized
|
|
703
|
+
// cost at the same moment as the content cost (per-item trust is CONTENT,
|
|
704
|
+
// not fixed scaffolding; see buildTrustEntry).
|
|
705
|
+
const trustEntry = includeTrust ? buildTrustEntry(m, "permanent") : null;
|
|
706
|
+
const trustCost = trustEntry ? estimateTokens(JSON.stringify(trustEntry)) : 0;
|
|
707
|
+
if (cost + trustCost <= tokenBudget) {
|
|
674
708
|
sections.permanent.push(line);
|
|
675
709
|
includedOwnMemories.push(struct);
|
|
676
|
-
if (
|
|
677
|
-
includedTrustMemories.push(
|
|
678
|
-
tokenBudget -= cost;
|
|
710
|
+
if (trustEntry)
|
|
711
|
+
includedTrustMemories.push(trustEntry);
|
|
712
|
+
tokenBudget -= cost + trustCost;
|
|
679
713
|
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
680
714
|
}
|
|
681
715
|
else {
|
|
@@ -743,16 +777,20 @@ export class BootstrapMemories extends Resource {
|
|
|
743
777
|
const line = formatMemory(m, agentId);
|
|
744
778
|
const struct = leanMemory(m, "recent");
|
|
745
779
|
const cost = contentCost(struct, line); // #1199 (0.44.11) — charge what ships; see contentCost
|
|
746
|
-
|
|
780
|
+
// flair#1199 trust-admission — charge the trust block's real serialized
|
|
781
|
+
// cost against BOTH the recent sub-budget and the shared tokenBudget.
|
|
782
|
+
const trustEntry = includeTrust ? buildTrustEntry(m, "recent") : null;
|
|
783
|
+
const trustCost = trustEntry ? estimateTokens(JSON.stringify(trustEntry)) : 0;
|
|
784
|
+
if (recentSpent + cost + trustCost > recentBudget) {
|
|
747
785
|
truncatedOwnIds.add(m.id); // #1207 — budget-skip; may still be admitted later via the task-relevant loop (deduped at the end)
|
|
748
786
|
continue;
|
|
749
787
|
}
|
|
750
788
|
sections.recent.push(line);
|
|
751
789
|
includedOwnMemories.push(struct);
|
|
752
|
-
if (
|
|
753
|
-
includedTrustMemories.push(
|
|
754
|
-
recentSpent += cost;
|
|
755
|
-
tokenBudget -= cost;
|
|
790
|
+
if (trustEntry)
|
|
791
|
+
includedTrustMemories.push(trustEntry);
|
|
792
|
+
recentSpent += cost + trustCost;
|
|
793
|
+
tokenBudget -= cost + trustCost;
|
|
756
794
|
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
757
795
|
}
|
|
758
796
|
// --- 3b. Subject-predicted context ---
|
|
@@ -787,16 +825,20 @@ export class BootstrapMemories extends Resource {
|
|
|
787
825
|
const line = formatMemory(m, agentId);
|
|
788
826
|
const struct = leanMemory(m, "predicted");
|
|
789
827
|
const cost = contentCost(struct, line); // #1199 (0.44.11) — charge what ships; see contentCost
|
|
790
|
-
|
|
828
|
+
// flair#1199 trust-admission — charge the trust block's real serialized
|
|
829
|
+
// cost against BOTH the predicted sub-budget and the shared tokenBudget.
|
|
830
|
+
const trustEntry = includeTrust ? buildTrustEntry(m, "predicted") : null;
|
|
831
|
+
const trustCost = trustEntry ? estimateTokens(JSON.stringify(trustEntry)) : 0;
|
|
832
|
+
if (predictedSpent + cost + trustCost > predictedBudget) {
|
|
791
833
|
truncatedOwnIds.add(m.id); // #1207 — budget-skip (deduped against inclusions at the end)
|
|
792
834
|
continue;
|
|
793
835
|
}
|
|
794
836
|
sections.predicted.push(line);
|
|
795
837
|
includedPredicted.push(struct);
|
|
796
|
-
if (
|
|
797
|
-
includedTrustMemories.push(
|
|
798
|
-
predictedSpent += cost;
|
|
799
|
-
tokenBudget -= cost;
|
|
838
|
+
if (trustEntry)
|
|
839
|
+
includedTrustMemories.push(trustEntry);
|
|
840
|
+
predictedSpent += cost + trustCost;
|
|
841
|
+
tokenBudget -= cost + trustCost;
|
|
800
842
|
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id; also the task-relevant loop's exclusion set (no predicted→relevant double-admit)
|
|
801
843
|
}
|
|
802
844
|
}
|
|
@@ -985,7 +1027,13 @@ export class BootstrapMemories extends Resource {
|
|
|
985
1027
|
}
|
|
986
1028
|
: leanMemory(m, "relevant");
|
|
987
1029
|
const cost = contentCost(struct, line);
|
|
988
|
-
|
|
1030
|
+
// flair#1199 trust-admission — charge the trust block's real serialized
|
|
1031
|
+
// cost at the same moment as the content cost. The section (teammate vs
|
|
1032
|
+
// relevant) is decided by `m._source`, so build the entry with the right
|
|
1033
|
+
// section before the budget check.
|
|
1034
|
+
const trustEntry = includeTrust ? buildTrustEntry(m, m._source ? "teammate" : "relevant") : null;
|
|
1035
|
+
const trustCost = trustEntry ? estimateTokens(JSON.stringify(trustEntry)) : 0;
|
|
1036
|
+
if (cost + trustCost > tokenBudget) {
|
|
989
1037
|
// flair#1207 — a size-skip in the score-ordered task-relevant loop
|
|
990
1038
|
// is no longer silent: record it on the denominator matching the
|
|
991
1039
|
// record's origin (own → truncatedOwnIds, teammate → the separate
|
|
@@ -1006,9 +1054,9 @@ export class BootstrapMemories extends Resource {
|
|
|
1006
1054
|
// memoriesIncluded — that different-denominator mix is what let
|
|
1007
1055
|
// included exceed available.
|
|
1008
1056
|
includedTeammateFindings.push(struct);
|
|
1009
|
-
if (
|
|
1010
|
-
includedTrustMemories.push(
|
|
1011
|
-
tokenBudget -= cost;
|
|
1057
|
+
if (trustEntry)
|
|
1058
|
+
includedTrustMemories.push(trustEntry);
|
|
1059
|
+
tokenBudget -= cost + trustCost;
|
|
1012
1060
|
teammateFindingsIncluded++;
|
|
1013
1061
|
}
|
|
1014
1062
|
else {
|
|
@@ -1016,9 +1064,9 @@ export class BootstrapMemories extends Resource {
|
|
|
1016
1064
|
// flair#1182 — own task-relevant records join the `memories`
|
|
1017
1065
|
// container.
|
|
1018
1066
|
includedOwnMemories.push(struct);
|
|
1019
|
-
if (
|
|
1020
|
-
includedTrustMemories.push(
|
|
1021
|
-
tokenBudget -= cost;
|
|
1067
|
+
if (trustEntry)
|
|
1068
|
+
includedTrustMemories.push(trustEntry);
|
|
1069
|
+
tokenBudget -= cost + trustCost;
|
|
1022
1070
|
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
1023
1071
|
}
|
|
1024
1072
|
}
|
|
@@ -1346,28 +1394,12 @@ export class BootstrapMemories extends Resource {
|
|
|
1346
1394
|
// caller's own records. flair#1225 (0.44.11) — the null on a lifecycle
|
|
1347
1395
|
// section is now SELF-EXPLAINING (matchQualityNote below), not just legible
|
|
1348
1396
|
// via `section`.
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
// surface, so there is no similarity to band — the null means "not
|
|
1356
|
-
// scored here", never a scoring failure on the caller's own records
|
|
1357
|
-
// (the #1201 misread: own-recent null beside a teammate band). Behavior
|
|
1358
|
-
// is unchanged (per Kern); this only makes the null self-describing in
|
|
1359
|
-
// the payload — Fix 3's "any absent field says why" — so a connector
|
|
1360
|
-
// reads it right without knowing the #1201 contract.
|
|
1361
|
-
const matchQualityNote = block.matchQuality === null
|
|
1362
|
-
? (LIFECYCLE_SECTIONS.has(section)
|
|
1363
|
-
? `matchQuality is null because '${section}' is a lifecycle-window section, not a retrieval `
|
|
1364
|
-
+ `surface — there is no relevance score to band. This is correct (per flair#1225), not a scoring failure.`
|
|
1365
|
-
: "matchQuality is null because no semantic similarity was attached to this result "
|
|
1366
|
-
+ "(e.g. a by-id read or a keyword-only degraded match).")
|
|
1367
|
-
: undefined;
|
|
1368
|
-
return { id: m.id, section, ...block, ...(matchQualityNote ? { matchQualityNote } : {}) };
|
|
1369
|
-
})
|
|
1370
|
-
: undefined;
|
|
1397
|
+
// flair#1199 trust-admission — the trust entries were built (and their real
|
|
1398
|
+
// serialized cost charged) at admission time via buildTrustEntry, so the
|
|
1399
|
+
// response tail just ships them as-is. The #1225 matchQualityNote logic now
|
|
1400
|
+
// lives in buildTrustEntry (single source of truth — the charged size and
|
|
1401
|
+
// the shipped size can never drift).
|
|
1402
|
+
const trust = includeTrust ? includedTrustMemories : undefined;
|
|
1371
1403
|
// flair#744 slice 2 — opt-in abstention verdict for the task-relevance
|
|
1372
1404
|
// surface. Present ONLY when `abstain` is requested (byte-identical to
|
|
1373
1405
|
// pre-slice-2 otherwise); scoped to whether any memory covered
|
|
@@ -126,29 +126,26 @@ export class SemanticSearch extends Resource {
|
|
|
126
126
|
let temporalBoost = 1.0;
|
|
127
127
|
if (q && !sinceDate) {
|
|
128
128
|
const lq = String(q).toLowerCase();
|
|
129
|
+
// flair#1245: a text-derived temporal match must ONLY nudge recency in
|
|
130
|
+
// ranking (temporalBoost, a soft multiplier applied in
|
|
131
|
+
// semantic-retrieval-core.ts) — it must NEVER derive a hard `sinceDate`
|
|
132
|
+
// exclusion. An incidental temporal word in the query TEXT (the #1245
|
|
133
|
+
// canary carried "today" inside a slogan) otherwise silently dropped
|
|
134
|
+
// every candidate older than the window → 0 results. Only the explicit
|
|
135
|
+
// `since` API param (set above, untouched here) still hard-filters.
|
|
129
136
|
if (/\btoday\b|\bthis morning\b|\bthis afternoon\b/.test(lq)) {
|
|
130
|
-
const d = new Date();
|
|
131
|
-
d.setHours(0, 0, 0, 0);
|
|
132
|
-
sinceDate = d;
|
|
133
137
|
temporalBoost = 1.5;
|
|
134
138
|
}
|
|
135
139
|
else if (/\byesterday\b/.test(lq)) {
|
|
136
|
-
const d = new Date();
|
|
137
|
-
d.setDate(d.getDate() - 1);
|
|
138
|
-
d.setHours(0, 0, 0, 0);
|
|
139
|
-
sinceDate = d;
|
|
140
140
|
temporalBoost = 1.3;
|
|
141
141
|
}
|
|
142
142
|
else if (/\bthis week\b|\blast few days\b/.test(lq)) {
|
|
143
|
-
sinceDate = new Date(Date.now() - 7 * 24 * 3600_000);
|
|
144
143
|
temporalBoost = 1.2;
|
|
145
144
|
}
|
|
146
145
|
else if (/\blast week\b/.test(lq)) {
|
|
147
|
-
sinceDate = new Date(Date.now() - 14 * 24 * 3600_000);
|
|
148
146
|
temporalBoost = 1.1;
|
|
149
147
|
}
|
|
150
148
|
else if (/\brecently\b|\blately\b/.test(lq)) {
|
|
151
|
-
sinceDate = new Date(Date.now() - 3 * 24 * 3600_000);
|
|
152
149
|
temporalBoost = 1.3;
|
|
153
150
|
}
|
|
154
151
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─── The single "is this a valid durability" writer-intent guard ────────────
|
|
3
|
+
*
|
|
4
|
+
* Mirror of resources/memory-visibility.ts's assertValidVisibility, for the
|
|
5
|
+
* durability enum. Same asymmetry, same reason:
|
|
6
|
+
*
|
|
7
|
+
* - READING an unknown durability must be permissive. A row written before a
|
|
8
|
+
* tier existed (or by a non-Python adapter) may hold anything, and the read
|
|
9
|
+
* side must keep resolving it exactly as before — defaultVisibilityForDurability
|
|
10
|
+
* treats any non-permanent/persistent string as the private branch, and that
|
|
11
|
+
* fail-safe must not change.
|
|
12
|
+
* - WRITING an unknown durability must be refused. Today an unknown value via
|
|
13
|
+
* raw REST (or a future non-Python adapter) is silently accepted and lands on
|
|
14
|
+
* the narrower private branch by accident — fail-safe, but unvalidated by
|
|
15
|
+
* contract. Refusing at the schema boundary makes it safe by construction and
|
|
16
|
+
* makes adk-flair's "validated server-side" claim true as written (flair#1238,
|
|
17
|
+
* from Sherlock's #1237 review).
|
|
18
|
+
*
|
|
19
|
+
* Deliberately has ZERO imports — same load-bearing reason as memory-visibility.ts:
|
|
20
|
+
* this module is a pure function + constant that any caller can import without
|
|
21
|
+
* dragging in "harper".
|
|
22
|
+
*/
|
|
23
|
+
/** The only values a WRITER may supply. */
|
|
24
|
+
export const WRITABLE_DURABILITIES = ["permanent", "persistent", "standard", "ephemeral"];
|
|
25
|
+
/**
|
|
26
|
+
* Reject a durability a writer supplied that is not one of the four valid values.
|
|
27
|
+
* Returns an error message, or null when the value is acceptable.
|
|
28
|
+
*
|
|
29
|
+
* `undefined`/`null` are accepted: omitting the field is how a caller asks for
|
|
30
|
+
* the default ("standard"), and that is a documented, intentional path.
|
|
31
|
+
*/
|
|
32
|
+
export function assertValidDurability(durability) {
|
|
33
|
+
if (durability === undefined || durability === null)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof durability === "string" && WRITABLE_DURABILITIES.includes(durability)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return (`durability must be ${WRITABLE_DURABILITIES.map((v) => `"${v}"`).join(" or ")} ` +
|
|
39
|
+
`(got: ${JSON.stringify(durability)}). Omit it to use the default "standard".`);
|
|
40
|
+
}
|
|
@@ -21,6 +21,9 @@ you already have, [Remote Server](../README.md#remote-server) is simpler and kee
|
|
|
21
21
|
|
|
22
22
|
## Quickstart
|
|
23
23
|
|
|
24
|
+
New user who just needs a reachable `FLAIR_URL` for Cursor / Grok Bot? Start at
|
|
25
|
+
[quickstart-fabric.md](quickstart-fabric.md). This page is the operator path.
|
|
26
|
+
|
|
24
27
|
### 1. Deploy the component
|
|
25
28
|
|
|
26
29
|
```bash
|
|
@@ -5,7 +5,7 @@ Flair runs in one of three shapes. Pick yours and follow only that path.
|
|
|
5
5
|
| You want to... | Shape | Start here |
|
|
6
6
|
|---|---|---|
|
|
7
7
|
| Run Flair on your own machine or VPS. `flair init` installs Harper, creates your agent identity, and you're running. | **Standalone local** | [standalone-local.md](standalone-local.md) |
|
|
8
|
-
| Run Flair on [Harper Fabric](https://www.harperdb.io/) — managed hosting, multi-region replication, no shell on the node. You deploy a component; agents connect over HTTPS. | **Hosted on Fabric** | [hosted-on-fabric.md](hosted-on-fabric.md) |
|
|
8
|
+
| Run Flair on [Harper Fabric](https://www.harperdb.io/) — managed hosting, multi-region replication, no shell on the node. You deploy a component; agents connect over HTTPS. | **Hosted on Fabric** | [quickstart-fabric.md](quickstart-fabric.md) (new-user URL) · [hosted-on-fabric.md](hosted-on-fabric.md) |
|
|
9
9
|
| Load Flair into a Harper instance you already run. In-process calls — no HTTP, no second process, no key to distribute. Over HTTP it is one memory API among many. | **Embedded in a Harper app** | [embedding-in-a-harper-app.md](embedding-in-a-harper-app.md) |
|
|
10
10
|
|
|
11
11
|
---
|
package/docs/deployment.md
CHANGED
|
@@ -116,10 +116,11 @@ Note: embeddings run on CPU in Docker (no Metal acceleration). Performance is ac
|
|
|
116
116
|
|
|
117
117
|
Deploying to a Harper Fabric cluster is a different mechanism from the installs above — `flair deploy` pushes Flair as a cluster component instead of `npm install -g`. To upgrade an already-deployed Fabric instance in place, use `FABRIC_USER=<admin> FABRIC_PASSWORD=<pass> flair upgrade --target <fabric-url>` (or `--fabric-password-file <path>` in place of the env var), not the local upgrade path. Inline `--fabric-user`/`--fabric-password` flags also work but are discouraged — both leak to shell history and `ps`. See [`docs/upgrade.md` — Upgrading a Fabric-deployed instance](upgrade.md#upgrading-a-fabric-deployed-instance) for the full walkthrough, including the automatic post-deploy fleet-convergence sweep.
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
New user who needs a reachable `FLAIR_URL` for Cursor / Grok Bot? Start at
|
|
120
|
+
[`docs/quickstart-fabric.md`](quickstart-fabric.md). For the hosted shape end to
|
|
121
|
+
end — when to choose it, ports and auth against a managed Fabric endpoint, pairing
|
|
122
|
+
local spokes to a hosted hub, and what you can and cannot observe without a shell
|
|
123
|
+
on the node — see [`docs/deploying-on-fabric.md`](deploying-on-fabric.md).
|
|
123
124
|
|
|
124
125
|
---
|
|
125
126
|
|
package/docs/hosted-on-fabric.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Deploy Flair as a component to a [Harper Fabric](https://www.harperdb.io/) instance. You do not run the Harper process yourself: managed hosting, multi-region replication, no shell on the node.
|
|
4
4
|
|
|
5
|
+
Need a public URL for Cursor / Grok Bot / cloud agents? Start at [quickstart-fabric.md](quickstart-fabric.md).
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Deploy
|
package/docs/integrations.md
CHANGED
|
@@ -195,7 +195,8 @@ If it has a custom memory protocol, the adapter pattern is small (~200 lines). L
|
|
|
195
195
|
|
|
196
196
|
## See also
|
|
197
197
|
|
|
198
|
-
- [Quickstart](quickstart.md) — `flair init` to working memory
|
|
198
|
+
- [Quickstart](quickstart.md) — `flair init` to working memory on a laptop
|
|
199
|
+
- [Fabric Quickstart](quickstart-fabric.md) — `flair deploy` to a reachable Harper Fabric URL
|
|
199
200
|
- [Embedding in a Harper app](embedding-in-a-harper-app.md) — run Flair as a component of your own Harper instance and call it in-process
|
|
200
201
|
- [Memory bridges](bridges.md) — import/export Flair ↔ Mem0, ChatGPT, claude-project, markdown, agentic-stack (five bridges shipped)
|
|
201
202
|
- [Federation](federation.md) — pair instances peer-to-peer for cross-machine sync
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Fabric Quick Start
|
|
2
|
+
|
|
3
|
+
From zero to a **reachable** Flair URL — so Cursor, Grok Bot, and cloud agents can actually hit it.
|
|
4
|
+
|
|
5
|
+
Laptop Flair from [`docs/quickstart.md`](quickstart.md) listens on `127.0.0.1:19926`. That loopback origin is not reachable from Grok Bot or Cursor cloud agents. This page is the start path when you need a public HTTPS origin.
|
|
6
|
+
|
|
7
|
+
Fabric is **Harper-hosted**, not a Flair-operated cloud. You deploy Flair as a component onto [Harper Fabric](https://www.harperdb.io/).
|
|
8
|
+
|
|
9
|
+
## 0. Prerequisites
|
|
10
|
+
|
|
11
|
+
**Node.js 22 or newer**, a user-writable npm global prefix (do not install with `sudo` — same rule as the [local Quick Start](quickstart.md#0-prerequisites)), and **a Harper Fabric account** ([harperdb.io](https://www.harperdb.io/)). You need the org name, cluster name, and admin credentials for that account.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node --version # v22.x.x or newer
|
|
15
|
+
npm i -g @tpsdev-ai/flair
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Lead with environment credentials so they stay out of `ps` and shell history:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export FABRIC_USER=<admin>
|
|
22
|
+
export FABRIC_PASSWORD=<pass>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Scripting? Use `--fabric-password-file <path>` (mode `0600`) instead of `FABRIC_PASSWORD`. Inline `--fabric-password` works and leaks — do not lead with it.
|
|
26
|
+
|
|
27
|
+
`FABRIC_ORG` / `FABRIC_CLUSTER` can stand in for the flags below. `--fabric-token` is accepted but **fails** — Fabric `deploy_component` is Basic-auth only.
|
|
28
|
+
|
|
29
|
+
## 1. Deploy
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Validate args and package layout without deploying
|
|
33
|
+
flair deploy --fabric-org <org> --fabric-cluster <cluster> --dry-run
|
|
34
|
+
|
|
35
|
+
flair deploy --fabric-org <org> --fabric-cluster <cluster>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The target defaults to `https://<cluster>.<org>.harperfabric.com`. Override with `--target` if your instance URL is different. `flair deploy` writes `FLAIR_PUBLIC_URL` to that same origin so OAuth and A2A discovery do not advertise loopback.
|
|
39
|
+
|
|
40
|
+
## 2. What success looks like
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
→ Deploying flair to https://<cluster>.<org>.harperfabric.com
|
|
44
|
+
|
|
45
|
+
✓ Flair vX.Y.Z deployed and verified serving
|
|
46
|
+
|
|
47
|
+
URL: https://<cluster>.<org>.harperfabric.com
|
|
48
|
+
Project: flair
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A fleet-verify table follows. That HTTPS origin is your `FLAIR_URL`.
|
|
52
|
+
|
|
53
|
+
Then set an admin password in Fabric Studio (Cluster Settings → Admin). `flair agent add` against a remote instance requires `--admin-pass` — it will not reuse `~/.flair/admin-pass` or `FLAIR_ADMIN_PASS` from your laptop.
|
|
54
|
+
|
|
55
|
+
## 3. Register an agent against the remote instance
|
|
56
|
+
|
|
57
|
+
`flair agent add` takes a positional id and `--target`. There is no `--remote` flag on this command (`--remote` belongs to `flair init`).
|
|
58
|
+
|
|
59
|
+
On Fabric, ops lives on the **same hostname at port 9925**, not the CLI's default "data port − 1" derivation (that would be `:442`, where nothing answers). Pass `--ops-target` explicitly: <!-- docs-freshness-allow: Fabric ops API port, not legacy data port -->
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export FLAIR_URL=https://<cluster>.<org>.harperfabric.com
|
|
63
|
+
|
|
64
|
+
# Fabric ops is :9925 on the same host, not derived :442. docs-freshness-allow: Fabric ops API
|
|
65
|
+
flair agent add mybot --target "$FLAIR_URL" --ops-target https://<cluster>.<org>.harperfabric.com:9925 --admin-pass <fabric-admin-password>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Keypair written: ~/.flair/keys/mybot.key
|
|
70
|
+
✅ Agent 'mybot' (mybot) registered (ops: https://<cluster>.<org>.harperfabric.com:9925) <!-- docs-freshness-allow: Fabric ops API -->
|
|
71
|
+
Private key: ~/.flair/keys/mybot.key
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The private key stays on **this machine**. The Fabric node stores only the public key.
|
|
75
|
+
|
|
76
|
+
## 4. Point the Cursor plugin at it
|
|
77
|
+
|
|
78
|
+
This is why Fabric is the recommended start for **Grok Bot / Cursor cloud agents**: they cannot see your laptop's `127.0.0.1:19926`.
|
|
79
|
+
|
|
80
|
+
In Cursor: **Plugins → Configure**
|
|
81
|
+
|
|
82
|
+
| Variable | Value |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `FLAIR_URL` | `https://<cluster>.<org>.harperfabric.com` |
|
|
85
|
+
| `FLAIR_AGENT_ID` | `mybot` (the id you just added) |
|
|
86
|
+
|
|
87
|
+
Those are the two plugin schema fields. Local Cursor's `npx` can use the key from step 3 at `~/.flair/keys/mybot.key`. A cloud agent's `npx` runs on a different machine — that VM needs the key (or host-env admin credentials). See [`packages/cursor-flair/README.md`](../packages/cursor-flair/README.md).
|
|
88
|
+
|
|
89
|
+
## 5. Verify
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
flair status --target "$FLAIR_URL"
|
|
93
|
+
FLAIR_URL="$FLAIR_URL" flair memory add --agent mybot "Fabric Quick Start is reachable"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`flair memory add` has no `--target`; it honors `FLAIR_URL`. Then in Cursor:
|
|
97
|
+
|
|
98
|
+
> Load my Flair bootstrap, then store a test memory
|
|
99
|
+
|
|
100
|
+
You should see `bootstrap` return soul + memories, then `memory_store` confirm an id.
|
|
101
|
+
|
|
102
|
+
## What's next
|
|
103
|
+
|
|
104
|
+
Federation, pairing spokes, upgrades (`flair upgrade --target`), ports, and what you can observe without a shell on the node: **[docs/deploying-on-fabric.md](deploying-on-fabric.md)**.
|
|
105
|
+
|
|
106
|
+
Still on a laptop only, no public URL needed: **[docs/quickstart.md](quickstart.md)**.
|
package/docs/quickstart.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
From zero to a persistent agent memory in five minutes.
|
|
4
4
|
|
|
5
|
+
> **Need a reachable URL (Cursor cloud / Grok Bot / another machine)?** This guide is the laptop path — `flair init` binds `127.0.0.1:19926`, which those clients cannot see. Deploy on Harper Fabric instead: **[docs/quickstart-fabric.md](quickstart-fabric.md)**.
|
|
6
|
+
|
|
5
7
|
## 0. Prerequisites
|
|
6
8
|
|
|
7
9
|
**Node.js 22 or newer.** No Docker, no database to install, no API keys — Flair runs in a single process and computes embeddings locally.
|
package/docs/standalone-local.md
CHANGED
|
@@ -235,7 +235,8 @@ Full walkthrough: [federation.md](federation.md).
|
|
|
235
235
|
## See also
|
|
236
236
|
|
|
237
237
|
- [deployment-shapes.md](deployment-shapes.md) — choose your shape
|
|
238
|
-
- [quickstart.md](quickstart.md) — zero to working in 5 minutes
|
|
238
|
+
- [quickstart.md](quickstart.md) — zero to working in 5 minutes (laptop)
|
|
239
|
+
- [quickstart-fabric.md](quickstart-fabric.md) — reachable Harper Fabric URL for Cursor cloud / Grok Bot
|
|
239
240
|
- [upgrade.md](upgrade.md) — full upgrade mechanics (re-embedding, rollback, downgrade)
|
|
240
241
|
- [federation.md](federation.md) — hub-and-spoke sync between instances
|
|
241
242
|
- [troubleshooting.md](troubleshooting.md) — common issues and automated diagnosis
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.13",
|
|
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",
|