@tpsdev-ai/flair-client 0.21.0 → 0.22.0
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 +2 -2
- package/dist/client.d.ts +88 -1
- package/dist/client.js +111 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @tpsdev-ai/flair-client
|
|
|
16
16
|
import { FlairClient } from '@tpsdev-ai/flair-client'
|
|
17
17
|
|
|
18
18
|
const flair = new FlairClient({
|
|
19
|
-
url: 'http://localhost:
|
|
19
|
+
url: 'http://localhost:19926', // or remote: https://flair.example.com
|
|
20
20
|
agentId: 'my-agent',
|
|
21
21
|
// keyPath auto-resolved from ~/.flair/keys/my-agent.key
|
|
22
22
|
})
|
|
@@ -108,7 +108,7 @@ Or use the Flair CLI directly:
|
|
|
108
108
|
|
|
109
109
|
| Option | Env | Default | Description |
|
|
110
110
|
|--------|-----|---------|-------------|
|
|
111
|
-
| `url` | `FLAIR_URL` | `http://localhost:
|
|
111
|
+
| `url` | `FLAIR_URL` | `http://localhost:19926` | Flair server URL |
|
|
112
112
|
| `agentId` | `FLAIR_AGENT_ID` | — | Agent identifier |
|
|
113
113
|
| `keyPath` | `FLAIR_KEY_DIR` | auto-resolved | Private key path |
|
|
114
114
|
| `timeoutMs` | — | `10000` | Request timeout |
|
package/dist/client.d.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* const results = await flair.memory.search('that thing')
|
|
8
8
|
* const ctx = await flair.bootstrap({ maxTokens: 4000 })
|
|
9
9
|
*/
|
|
10
|
-
import type { FlairClientConfig, Memory, MemoryType, Durability, Visibility, SoulEntry, SearchResult, BootstrapResult } from "./types.js";
|
|
10
|
+
import type { FlairClientConfig, Memory, MemoryType, Durability, Visibility, SoulEntry, SearchResult, BootstrapResult, Relationship } from "./types.js";
|
|
11
11
|
export declare class FlairClient {
|
|
12
12
|
readonly url: string;
|
|
13
13
|
readonly agentId: string;
|
|
14
14
|
readonly memory: MemoryApi;
|
|
15
|
+
readonly relationship: RelationshipApi;
|
|
15
16
|
readonly soul: SoulApi;
|
|
16
17
|
private privateKey;
|
|
17
18
|
private keyResolved;
|
|
@@ -128,6 +129,92 @@ declare class MemoryApi {
|
|
|
128
129
|
/** Delete a memory. */
|
|
129
130
|
delete(id: string): Promise<void>;
|
|
130
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Canonical, per-owner, deterministic Relationship id (relationship-write-path,
|
|
134
|
+
* K&S-approved refinement) — `base64url(SHA-256(lowercased agentId+subject+
|
|
135
|
+
* predicate+object))`, truncated to the first 16 bytes (22 base64url chars,
|
|
136
|
+
* 128-bit collision resistance). A REAL cryptographic hash on purpose
|
|
137
|
+
* (`crypto.createHash('sha256')`, NOT `Bun.hash` or any weak/platform-specific
|
|
138
|
+
* hash) — Sherlock: blocks intentional collision attacks; Kern: portable if
|
|
139
|
+
* flair ever runs on another runtime.
|
|
140
|
+
*
|
|
141
|
+
* Hashes ONLY the triple-identity fields (agentId + subject + predicate +
|
|
142
|
+
* object) — deliberately EXCLUDES confidence/validFrom/validTo/source, so
|
|
143
|
+
* re-asserting the SAME triple with different mutable fields always maps to
|
|
144
|
+
* the SAME id. Because Relationship.put() is a PUT-by-primary-key (Harper
|
|
145
|
+
* table semantics — see resources/Relationship.ts), writing to that same id
|
|
146
|
+
* again is a natural upsert: mutable fields update, the id stays stable, no
|
|
147
|
+
* pre-insert query and no race. Fields are joined with a NUL separator before
|
|
148
|
+
* hashing (not naive string concatenation) so e.g. agentId="a"+subject="bc"
|
|
149
|
+
* can never hash identically to agentId="ab"+subject="c" — free-text
|
|
150
|
+
* subject/predicate/object have no natural delimiter of their own.
|
|
151
|
+
*
|
|
152
|
+
* `agentId` is folded into the hash so the canonical id is PER-OWNER — two
|
|
153
|
+
* different agents asserting the identical (subject, predicate, object)
|
|
154
|
+
* triple get two different ids, never a cross-agent collision/overwrite (the
|
|
155
|
+
* write path also stamps `agentId` from the server-verified auth verdict,
|
|
156
|
+
* never the body — see resources/Relationship.ts's put() — so even a
|
|
157
|
+
* maliciously-crafted URL id can't make a foreign agent's row visible to the
|
|
158
|
+
* wrong owner; it can only self-collide with the calling agent's own rows).
|
|
159
|
+
*
|
|
160
|
+
* Exported (not just used internally by RelationshipApi.write()) so the CLI's
|
|
161
|
+
* mirrored implementation (src/cli.ts's `relationship add` command, which
|
|
162
|
+
* cannot import this workspace package into the published `@tpsdev-ai/flair`
|
|
163
|
+
* CLI bundle — same reasoning as its Memory-id-generation mirroring) can be
|
|
164
|
+
* cross-checked against this one in tests, and so any other integration
|
|
165
|
+
* package can compute the same id a relationship will land at without a
|
|
166
|
+
* round-trip.
|
|
167
|
+
*/
|
|
168
|
+
export declare function canonicalRelationshipId(agentId: string, subject: string, predicate: string, object: string): string;
|
|
169
|
+
declare class RelationshipApi {
|
|
170
|
+
private client;
|
|
171
|
+
constructor(client: FlairClient);
|
|
172
|
+
/**
|
|
173
|
+
* Assert (write) a relationship triple: "record that <subject> <predicate>
|
|
174
|
+
* <object>". Always writes to the canonical id (canonicalRelationshipId
|
|
175
|
+
* above), so:
|
|
176
|
+
*
|
|
177
|
+
* (a) Re-asserting the SAME triple (same subject/predicate/object, same
|
|
178
|
+
* agentId) UPSERTS the existing row — mutable fields (confidence,
|
|
179
|
+
* validFrom/validTo, source) update, the id and createdAt-derived
|
|
180
|
+
* identity stay stable. No duplicate rows from re-assertion.
|
|
181
|
+
* (b) A CONTRADICTING triple with the same subject/predicate/object but a
|
|
182
|
+
* different `validTo` OVERWRITES the prior row's validTo too (the
|
|
183
|
+
* old value is lost) — acceptable: the graph wants the CURRENT state
|
|
184
|
+
* of a relationship, not a full history chain (Memory's
|
|
185
|
+
* supersedes-chain is overkill here).
|
|
186
|
+
* (c) A DIFFERENT predicate (e.g. "nathan manages flair" superseded by
|
|
187
|
+
* "nathan advises flair") hashes to a DIFFERENT id — a NEW row, and
|
|
188
|
+
* the OLD triple is NOT auto-closed. To contradict a prior
|
|
189
|
+
* relationship under a different predicate, set its `validTo` (via a
|
|
190
|
+
* second write with the OLD subject/predicate/object) or delete it,
|
|
191
|
+
* THEN write the new one.
|
|
192
|
+
*
|
|
193
|
+
* Never suppresses the write (same invariant as MemoryApi.write() — see
|
|
194
|
+
* flair#526's history for why "found something similar, don't write" is
|
|
195
|
+
* the wrong default): dedup here is pure upsert-by-canonical-id, not a
|
|
196
|
+
* near-duplicate signal.
|
|
197
|
+
*/
|
|
198
|
+
write(input: {
|
|
199
|
+
subject: string;
|
|
200
|
+
predicate: string;
|
|
201
|
+
object: string;
|
|
202
|
+
/** 0.0–1.0, how certain (1.0 = explicitly stated). Server default: 1.0. */
|
|
203
|
+
confidence?: number;
|
|
204
|
+
/** ISO timestamp — when this relationship became true. Server default: now. */
|
|
205
|
+
validFrom?: string;
|
|
206
|
+
/** ISO timestamp — when it ended. Leave unset for an active relationship;
|
|
207
|
+
* set it on a prior write to close out a relationship you're contradicting. */
|
|
208
|
+
validTo?: string;
|
|
209
|
+
/** Where this was learned (memory ID, conversation, etc.). */
|
|
210
|
+
source?: string;
|
|
211
|
+
}): Promise<Relationship>;
|
|
212
|
+
/** Get a relationship by canonical id (or any id, e.g. one openclaw wrote
|
|
213
|
+
* under its own convention). Returns null on 404 (not found / not yours). */
|
|
214
|
+
get(id: string): Promise<Relationship | null>;
|
|
215
|
+
/** Delete a relationship by id. */
|
|
216
|
+
delete(id: string): Promise<void>;
|
|
217
|
+
}
|
|
131
218
|
declare class SoulApi {
|
|
132
219
|
private client;
|
|
133
220
|
constructor(client: FlairClient);
|
package/dist/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* const results = await flair.memory.search('that thing')
|
|
8
8
|
* const ctx = await flair.bootstrap({ maxTokens: 4000 })
|
|
9
9
|
*/
|
|
10
|
-
import { createPrivateKey } from "node:crypto";
|
|
10
|
+
import { createHash, createPrivateKey } from "node:crypto";
|
|
11
11
|
import { loadPrivateKey, resolveKeyPath, signRequest } from "./auth.js";
|
|
12
12
|
const DEFAULT_URL = "http://localhost:19926";
|
|
13
13
|
const DEFAULT_TIMEOUT = 30_000;
|
|
@@ -15,6 +15,7 @@ export class FlairClient {
|
|
|
15
15
|
url;
|
|
16
16
|
agentId;
|
|
17
17
|
memory;
|
|
18
|
+
relationship;
|
|
18
19
|
soul;
|
|
19
20
|
privateKey = null;
|
|
20
21
|
keyResolved = false;
|
|
@@ -37,6 +38,7 @@ export class FlairClient {
|
|
|
37
38
|
this.basicAuth = `Basic ${Buffer.from(`${adminUser}:${adminPass}`).toString("base64")}`;
|
|
38
39
|
}
|
|
39
40
|
this.memory = new MemoryApi(this);
|
|
41
|
+
this.relationship = new RelationshipApi(this);
|
|
40
42
|
this.soul = new SoulApi(this);
|
|
41
43
|
}
|
|
42
44
|
resolveKey() {
|
|
@@ -285,6 +287,114 @@ class MemoryApi {
|
|
|
285
287
|
await this.client.request("DELETE", `/Memory/${id}`);
|
|
286
288
|
}
|
|
287
289
|
}
|
|
290
|
+
// ─── Relationship API ───────────────────────────────────────────────────────
|
|
291
|
+
/**
|
|
292
|
+
* Canonical, per-owner, deterministic Relationship id (relationship-write-path,
|
|
293
|
+
* K&S-approved refinement) — `base64url(SHA-256(lowercased agentId+subject+
|
|
294
|
+
* predicate+object))`, truncated to the first 16 bytes (22 base64url chars,
|
|
295
|
+
* 128-bit collision resistance). A REAL cryptographic hash on purpose
|
|
296
|
+
* (`crypto.createHash('sha256')`, NOT `Bun.hash` or any weak/platform-specific
|
|
297
|
+
* hash) — Sherlock: blocks intentional collision attacks; Kern: portable if
|
|
298
|
+
* flair ever runs on another runtime.
|
|
299
|
+
*
|
|
300
|
+
* Hashes ONLY the triple-identity fields (agentId + subject + predicate +
|
|
301
|
+
* object) — deliberately EXCLUDES confidence/validFrom/validTo/source, so
|
|
302
|
+
* re-asserting the SAME triple with different mutable fields always maps to
|
|
303
|
+
* the SAME id. Because Relationship.put() is a PUT-by-primary-key (Harper
|
|
304
|
+
* table semantics — see resources/Relationship.ts), writing to that same id
|
|
305
|
+
* again is a natural upsert: mutable fields update, the id stays stable, no
|
|
306
|
+
* pre-insert query and no race. Fields are joined with a NUL separator before
|
|
307
|
+
* hashing (not naive string concatenation) so e.g. agentId="a"+subject="bc"
|
|
308
|
+
* can never hash identically to agentId="ab"+subject="c" — free-text
|
|
309
|
+
* subject/predicate/object have no natural delimiter of their own.
|
|
310
|
+
*
|
|
311
|
+
* `agentId` is folded into the hash so the canonical id is PER-OWNER — two
|
|
312
|
+
* different agents asserting the identical (subject, predicate, object)
|
|
313
|
+
* triple get two different ids, never a cross-agent collision/overwrite (the
|
|
314
|
+
* write path also stamps `agentId` from the server-verified auth verdict,
|
|
315
|
+
* never the body — see resources/Relationship.ts's put() — so even a
|
|
316
|
+
* maliciously-crafted URL id can't make a foreign agent's row visible to the
|
|
317
|
+
* wrong owner; it can only self-collide with the calling agent's own rows).
|
|
318
|
+
*
|
|
319
|
+
* Exported (not just used internally by RelationshipApi.write()) so the CLI's
|
|
320
|
+
* mirrored implementation (src/cli.ts's `relationship add` command, which
|
|
321
|
+
* cannot import this workspace package into the published `@tpsdev-ai/flair`
|
|
322
|
+
* CLI bundle — same reasoning as its Memory-id-generation mirroring) can be
|
|
323
|
+
* cross-checked against this one in tests, and so any other integration
|
|
324
|
+
* package can compute the same id a relationship will land at without a
|
|
325
|
+
* round-trip.
|
|
326
|
+
*/
|
|
327
|
+
export function canonicalRelationshipId(agentId, subject, predicate, object) {
|
|
328
|
+
const material = [agentId, subject, predicate, object].join("\u0000").toLowerCase();
|
|
329
|
+
return createHash("sha256").update(material, "utf8").digest().subarray(0, 16).toString("base64url");
|
|
330
|
+
}
|
|
331
|
+
class RelationshipApi {
|
|
332
|
+
client;
|
|
333
|
+
constructor(client) {
|
|
334
|
+
this.client = client;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Assert (write) a relationship triple: "record that <subject> <predicate>
|
|
338
|
+
* <object>". Always writes to the canonical id (canonicalRelationshipId
|
|
339
|
+
* above), so:
|
|
340
|
+
*
|
|
341
|
+
* (a) Re-asserting the SAME triple (same subject/predicate/object, same
|
|
342
|
+
* agentId) UPSERTS the existing row — mutable fields (confidence,
|
|
343
|
+
* validFrom/validTo, source) update, the id and createdAt-derived
|
|
344
|
+
* identity stay stable. No duplicate rows from re-assertion.
|
|
345
|
+
* (b) A CONTRADICTING triple with the same subject/predicate/object but a
|
|
346
|
+
* different `validTo` OVERWRITES the prior row's validTo too (the
|
|
347
|
+
* old value is lost) — acceptable: the graph wants the CURRENT state
|
|
348
|
+
* of a relationship, not a full history chain (Memory's
|
|
349
|
+
* supersedes-chain is overkill here).
|
|
350
|
+
* (c) A DIFFERENT predicate (e.g. "nathan manages flair" superseded by
|
|
351
|
+
* "nathan advises flair") hashes to a DIFFERENT id — a NEW row, and
|
|
352
|
+
* the OLD triple is NOT auto-closed. To contradict a prior
|
|
353
|
+
* relationship under a different predicate, set its `validTo` (via a
|
|
354
|
+
* second write with the OLD subject/predicate/object) or delete it,
|
|
355
|
+
* THEN write the new one.
|
|
356
|
+
*
|
|
357
|
+
* Never suppresses the write (same invariant as MemoryApi.write() — see
|
|
358
|
+
* flair#526's history for why "found something similar, don't write" is
|
|
359
|
+
* the wrong default): dedup here is pure upsert-by-canonical-id, not a
|
|
360
|
+
* near-duplicate signal.
|
|
361
|
+
*/
|
|
362
|
+
async write(input) {
|
|
363
|
+
const id = canonicalRelationshipId(this.client.agentId, input.subject, input.predicate, input.object);
|
|
364
|
+
const record = {
|
|
365
|
+
id,
|
|
366
|
+
subject: input.subject,
|
|
367
|
+
predicate: input.predicate,
|
|
368
|
+
object: input.object,
|
|
369
|
+
};
|
|
370
|
+
if (input.confidence !== undefined)
|
|
371
|
+
record.confidence = input.confidence;
|
|
372
|
+
if (input.validFrom !== undefined)
|
|
373
|
+
record.validFrom = input.validFrom;
|
|
374
|
+
if (input.validTo !== undefined)
|
|
375
|
+
record.validTo = input.validTo;
|
|
376
|
+
if (input.source !== undefined)
|
|
377
|
+
record.source = input.source;
|
|
378
|
+
const response = await this.client.request("PUT", `/Relationship/${id}`, record);
|
|
379
|
+
return { ...record, id, agentId: this.client.agentId, ...(response ?? {}) };
|
|
380
|
+
}
|
|
381
|
+
/** Get a relationship by canonical id (or any id, e.g. one openclaw wrote
|
|
382
|
+
* under its own convention). Returns null on 404 (not found / not yours). */
|
|
383
|
+
async get(id) {
|
|
384
|
+
try {
|
|
385
|
+
return await this.client.request("GET", `/Relationship/${id}`);
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
if (e instanceof FlairError && e.status === 404)
|
|
389
|
+
return null;
|
|
390
|
+
throw e;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** Delete a relationship by id. */
|
|
394
|
+
async delete(id) {
|
|
395
|
+
await this.client.request("DELETE", `/Relationship/${id}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
288
398
|
// ─── Soul API ───────────────────────────────────────────────────────────────
|
|
289
399
|
class SoulApi {
|
|
290
400
|
client;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { FlairClient, FlairError } from "./client.js";
|
|
1
|
+
export { FlairClient, FlairError, canonicalRelationshipId } from "./client.js";
|
|
2
2
|
export { loadPrivateKey, resolveKeyPath, signRequest } from "./auth.js";
|
|
3
|
-
export type { FlairClientConfig, Memory, MemoryType, Durability, Visibility, SoulEntry, SearchResult, BootstrapResult, } from "./types.js";
|
|
3
|
+
export type { FlairClientConfig, Memory, MemoryType, Durability, Visibility, SoulEntry, SearchResult, BootstrapResult, Relationship, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { FlairClient, FlairError } from "./client.js";
|
|
1
|
+
export { FlairClient, FlairError, canonicalRelationshipId } from "./client.js";
|
|
2
2
|
export { loadPrivateKey, resolveKeyPath, signRequest } from "./auth.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -54,6 +54,36 @@ export interface Memory {
|
|
|
54
54
|
*/
|
|
55
55
|
deduped?: boolean;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* An entity-to-entity relationship triple (subject/predicate/object), with
|
|
59
|
+
* temporal validity and per-owner canonical dedup (see RelationshipApi.write()
|
|
60
|
+
* in client.ts for the canonical-id scheme). Free-text, lowercased on write —
|
|
61
|
+
* NOT the attention-plane `type:value` entity vocabulary, NOT memory-id FKs.
|
|
62
|
+
*/
|
|
63
|
+
export interface Relationship {
|
|
64
|
+
id: string;
|
|
65
|
+
agentId: string;
|
|
66
|
+
subject: string;
|
|
67
|
+
predicate: string;
|
|
68
|
+
object: string;
|
|
69
|
+
/** ISO timestamp — when this relationship became true. */
|
|
70
|
+
validFrom?: string;
|
|
71
|
+
/** ISO timestamp — when it ended (absent/null = still active). */
|
|
72
|
+
validTo?: string;
|
|
73
|
+
/** 0.0–1.0, how certain (1.0 = explicitly stated). Defaults server-side to 1.0. */
|
|
74
|
+
confidence?: number;
|
|
75
|
+
/** Where this was learned (memory ID, conversation, etc.). */
|
|
76
|
+
source?: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
updatedAt?: string;
|
|
79
|
+
/** JSON blob, same shape as Memory.provenance — { v, verified: { agentId,
|
|
80
|
+
* timestamp }, claimed?: { model } }. Absent on rows written before this
|
|
81
|
+
* field existed (migration-equivalence: additive/nullable). */
|
|
82
|
+
provenance?: string;
|
|
83
|
+
/** Always true after a successful write() — the server never suppresses a
|
|
84
|
+
* relationship write. */
|
|
85
|
+
written?: boolean;
|
|
86
|
+
}
|
|
57
87
|
/** A soul entry (persistent personality/values). */
|
|
58
88
|
export interface SoulEntry {
|
|
59
89
|
id: string;
|
package/package.json
CHANGED