@tpsdev-ai/flair 0.49.0 → 0.51.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.
Files changed (39) hide show
  1. package/README.md +15 -4
  2. package/dist/bridges/runtime/roundtrip.js +91 -2
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli.js +565 -171
  5. package/dist/deploy.js +20 -3
  6. package/dist/doctor-client.js +62 -34
  7. package/dist/federation/scheduler.js +24 -3
  8. package/dist/hook-install.js +45 -13
  9. package/dist/install/clients.js +28 -17
  10. package/dist/lib/doctor-run.js +481 -0
  11. package/dist/lib/launchd-management.js +7 -26
  12. package/dist/lib/scheduler-platform.js +132 -10
  13. package/dist/lib/scratch-owner.js +49 -0
  14. package/dist/rem/scheduler.js +23 -5
  15. package/dist/resources/Federation.js +42 -20
  16. package/dist/resources/MemoryBootstrap.js +8 -4
  17. package/dist/resources/RecordUsage.js +13 -6
  18. package/dist/resources/SemanticSearch.js +8 -1
  19. package/dist/resources/federation-classify.js +90 -0
  20. package/dist/resources/health.js +51 -7
  21. package/dist/resources/mcp-tools.js +10 -6
  22. package/dist/resources/search-readiness.js +123 -0
  23. package/dist/resources/semantic-retrieval-core.js +48 -21
  24. package/dist/resources/sort-comparators.js +45 -0
  25. package/dist/resources/usage-ids.js +63 -0
  26. package/dist/src/lib/scheduler-platform.js +132 -10
  27. package/dist/src/rem/scheduler.js +23 -5
  28. package/docs/auth.md +5 -0
  29. package/docs/deepseek-harness.md +1 -1
  30. package/docs/federation.md +11 -0
  31. package/docs/hosted-on-fabric.md +2 -0
  32. package/docs/integrations.md +53 -1
  33. package/docs/mcp-clients.md +67 -15
  34. package/docs/quickstart-fabric.md +1 -1
  35. package/docs/supply-chain-policy.md +1 -1
  36. package/docs/troubleshooting.md +25 -0
  37. package/docs/upgrade.md +17 -1
  38. package/package.json +3 -3
  39. package/schemas/federation.graphql +1 -1
package/README.md CHANGED
@@ -16,17 +16,28 @@ Runs on a laptop, a VPS, or anywhere Node does. Needs **Node.js 22+**.
16
16
  # 1. Install the CLI (no sudo)
17
17
  npm install -g @tpsdev-ai/flair
18
18
 
19
- # 2. Bootstrap the instance and register an agent
19
+ # 2. Verify the command is on your PATH
20
+ flair --version
21
+
22
+ # 3. Bootstrap the instance and register an agent
20
23
  flair init --agent mybot
21
24
 
22
- # 3. Write a memory
25
+ # 4. Write a memory
23
26
  flair memory add --agent mybot "Harper v5 sandbox blocks node:module but process.dlopen works"
24
27
 
25
- # 4. Find it back by meaning, not by keyword
28
+ # 5. Find it back by meaning, not by keyword
26
29
  flair search --agent mybot "native addon loading in sandboxed runtimes"
27
30
  ```
28
31
 
29
- Step 4 finds the memory you never keyword-matched:
32
+ If `flair --version` prints `command not found`, your npm global bin directory is not on `PATH`:
33
+
34
+ ```bash
35
+ export PATH="$(npm prefix -g)/bin:$PATH"
36
+ ```
37
+
38
+ Re-run `flair --version` to confirm, then continue.
39
+
40
+ Step 5 finds the memory you never keyword-matched:
30
41
 
31
42
  ```
32
43
  Harper v5 sandbox blocks node:module but process.dlopen works
@@ -16,7 +16,7 @@
16
16
  * Implementation note: does NOT talk to Flair. Fixture-to-fixture only.
17
17
  * Lives here so slice-3c code plugins can reuse the same harness.
18
18
  */
19
- import { promises as fsp } from "node:fs";
19
+ import { promises as fsp, readdirSync, rmSync, statSync } from "node:fs";
20
20
  import { join, isAbsolute } from "node:path";
21
21
  import { tmpdir } from "node:os";
22
22
  import { randomUUID } from "node:crypto";
@@ -25,6 +25,74 @@ import { parseRecords } from "./formats.js";
25
25
  import { applyMap } from "./mapper.js";
26
26
  import { evaluatePredicate } from "./predicate.js";
27
27
  import { writeRecords } from "./writers.js";
28
+ import { hasScratchOwnerStamp, scratchOwnerIsLive, writeScratchOwnerStamp, } from "../../lib/scratch-owner.js";
29
+ /** mkdtemp prefix for this harness. Leaked trees of this name filled a host
30
+ * to zero bytes (flair#1032) — 7,188 leftovers, mostly `…-test-bridge-*`. */
31
+ export const BRIDGE_TEST_DIR_PREFIX = "flair-bridge-test-";
32
+ /** Grace for *unstamped* leftovers only (stamp not written yet, or a tree
33
+ * from before the owner file existed). Stamped trees use pid liveness. */
34
+ export const STALE_BRIDGE_TEST_DIR_MS = 60_000;
35
+ const IN_FLIGHT_BRIDGE_TEST_DIRS = new Set();
36
+ let bridgeTestExitHookInstalled = false;
37
+ function installBridgeTestExitHook() {
38
+ if (bridgeTestExitHookInstalled)
39
+ return;
40
+ bridgeTestExitHookInstalled = true;
41
+ // Exit only — no signal handlers. The integration suite SIGTERMs the
42
+ // runner as a fixture (federation-watch); a handler here would truncate it.
43
+ process.on("exit", () => {
44
+ for (const dir of IN_FLIGHT_BRIDGE_TEST_DIRS) {
45
+ try {
46
+ rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ catch { /* best effort */ }
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Remove leftover `flair-bridge-test-*` trees from earlier interrupted runs.
54
+ *
55
+ * A process-exit hook cannot cover SIGKILL or a hard crash, so the next
56
+ * harness start has to sweep what the last one left. Skips in-flight trees
57
+ * in this process and any tree whose owner pid is still alive. Directory
58
+ * mtime is not a liveness signal (Linux does not update it when files
59
+ * inside subdirs are appended) — it is only a race guard for unstamped dirs.
60
+ *
61
+ * @returns number of trees removed
62
+ */
63
+ export function sweepStaleBridgeTestDirs(opts) {
64
+ const root = opts?.root ?? tmpdir();
65
+ const olderThanMs = opts?.olderThanMs ?? STALE_BRIDGE_TEST_DIR_MS;
66
+ const now = Date.now();
67
+ let names;
68
+ try {
69
+ names = readdirSync(root);
70
+ }
71
+ catch {
72
+ return 0;
73
+ }
74
+ let removed = 0;
75
+ for (const name of names) {
76
+ if (!name.startsWith(BRIDGE_TEST_DIR_PREFIX))
77
+ continue;
78
+ const dir = join(root, name);
79
+ if (IN_FLIGHT_BRIDGE_TEST_DIRS.has(dir))
80
+ continue;
81
+ if (scratchOwnerIsLive(dir))
82
+ continue;
83
+ try {
84
+ if (!hasScratchOwnerStamp(dir)) {
85
+ const st = statSync(dir);
86
+ if (now - st.mtimeMs < olderThanMs)
87
+ continue;
88
+ }
89
+ rmSync(dir, { recursive: true, force: true });
90
+ removed++;
91
+ }
92
+ catch { /* gone, or not ours to remove */ }
93
+ }
94
+ return removed;
95
+ }
28
96
  /** Fields the spec (§8) requires to survive round-trip. */
29
97
  export const ROUND_TRIP_STABLE_FIELDS = ["content", "subject", "tags", "durability"];
30
98
  export async function runRoundTrip(opts) {
@@ -52,8 +120,29 @@ export async function runRoundTrip(opts) {
52
120
  const importSource = descriptor.import.sources[0];
53
121
  const exportTarget = descriptor.export.targets[0];
54
122
  const resolvedFixture = resolvePath(cwd, opts.fixturePath ?? importSource.path);
55
- const tmpDir = await fsp.mkdtemp(join(tmpdir(), `flair-bridge-test-${descriptor.name}-`));
123
+ // Sweep leftovers from interrupted runs *before* creating ours so a crash
124
+ // mid-call is all the next start has to find. In-flight trees in this
125
+ // process are skipped inside the sweep.
126
+ sweepStaleBridgeTestDirs();
127
+ const tmpDir = await fsp.mkdtemp(join(tmpdir(), `${BRIDGE_TEST_DIR_PREFIX}${descriptor.name}-`));
128
+ writeScratchOwnerStamp(tmpDir);
56
129
  const tmpPath = join(tmpDir, `roundtrip.${suffixForFormat(exportTarget.format)}`);
130
+ IN_FLIGHT_BRIDGE_TEST_DIRS.add(tmpDir);
131
+ installBridgeTestExitHook();
132
+ try {
133
+ return await runRoundTripAgainst(descriptor, resolvedFixture, importSource, exportTarget, tmpPath);
134
+ }
135
+ finally {
136
+ IN_FLIGHT_BRIDGE_TEST_DIRS.delete(tmpDir);
137
+ if (!opts.retainTmpDir) {
138
+ try {
139
+ await fsp.rm(tmpDir, { recursive: true, force: true });
140
+ }
141
+ catch { /* best effort */ }
142
+ }
143
+ }
144
+ }
145
+ async function runRoundTripAgainst(descriptor, resolvedFixture, importSource, exportTarget, tmpPath) {
57
146
  // Pass 1: fixture → BridgeMemory[]
58
147
  const pass1 = [];
59
148
  for await (const { record } of parseRecords(descriptor.name, resolvedFixture, importSource.format)) {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.49.0",
3
- "commit": "e6336fcbb25c46fcd53b00488d695659c61cc420",
4
- "builtAt": "2026-08-24T19:36:52.866Z",
2
+ "version": "0.51.0",
3
+ "commit": "92b5d12cc3aafe892e90af657f073ef88b180ba6",
4
+ "builtAt": "2026-08-30T16:10:24.457Z",
5
5
  "builder": "tsc"
6
6
  }