@tpsdev-ai/flair 0.5.6 → 0.6.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 +3 -0
- package/dist/bridges/builtins/agentic-stack.js +58 -0
- package/dist/bridges/builtins/index.js +36 -0
- package/dist/bridges/discover.js +195 -0
- package/dist/bridges/runtime/context.js +59 -0
- package/dist/bridges/runtime/execute.js +92 -0
- package/dist/bridges/runtime/export-runner.js +105 -0
- package/dist/bridges/runtime/formats.js +136 -0
- package/dist/bridges/runtime/import-runner.js +107 -0
- package/dist/bridges/runtime/load-descriptor.js +46 -0
- package/dist/bridges/runtime/mapper.js +139 -0
- package/dist/bridges/runtime/predicate.js +122 -0
- package/dist/bridges/runtime/writers.js +130 -0
- package/dist/bridges/runtime/yaml-loader.js +148 -0
- package/dist/bridges/scaffold.js +224 -0
- package/dist/bridges/types.js +30 -0
- package/dist/cli.js +970 -216
- package/dist/deploy.js +161 -0
- package/dist/resources/ObservationCenter.js +19 -0
- package/dist/resources/health.js +443 -14
- package/package.json +6 -6
- package/ui/observation-center.html +91 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML bridge descriptor loader.
|
|
3
|
+
*
|
|
4
|
+
* Reads a `.flair-bridge/<name>.yaml` (or `~/.flair/bridges/<name>.yaml`),
|
|
5
|
+
* parses it with js-yaml, and normalizes it into a typed
|
|
6
|
+
* `YamlBridgeDescriptor`. Validation is strict for required fields
|
|
7
|
+
* (`name`, `kind`, at least one of `import`/`export`), lenient elsewhere —
|
|
8
|
+
* unknown fields are preserved but ignored; the spec is allowed to grow.
|
|
9
|
+
*
|
|
10
|
+
* Errors are always `BridgeRuntimeError` with LLM-readable `{field, expected, got, hint}`.
|
|
11
|
+
*/
|
|
12
|
+
import { promises as fsp } from "node:fs";
|
|
13
|
+
import yaml from "js-yaml";
|
|
14
|
+
import { BridgeRuntimeError } from "../types.js";
|
|
15
|
+
const VALID_FORMATS = [
|
|
16
|
+
"jsonl",
|
|
17
|
+
"json",
|
|
18
|
+
"yaml",
|
|
19
|
+
"markdown-frontmatter",
|
|
20
|
+
];
|
|
21
|
+
function failLoad(path, field, expected, got, hint) {
|
|
22
|
+
throw new BridgeRuntimeError({
|
|
23
|
+
bridge: "(unknown)",
|
|
24
|
+
op: "import",
|
|
25
|
+
path,
|
|
26
|
+
field,
|
|
27
|
+
expected,
|
|
28
|
+
got: typeof got === "string" ? got : JSON.stringify(got),
|
|
29
|
+
hint,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function fail(bridge, path, field, expected, got, hint) {
|
|
33
|
+
throw new BridgeRuntimeError({
|
|
34
|
+
bridge,
|
|
35
|
+
op: "import",
|
|
36
|
+
path,
|
|
37
|
+
field,
|
|
38
|
+
expected,
|
|
39
|
+
got: typeof got === "string" ? got : JSON.stringify(got),
|
|
40
|
+
hint,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function normalizeSourceTarget(bridge, path, fieldBase, raw) {
|
|
44
|
+
if (typeof raw !== "object" || raw === null) {
|
|
45
|
+
fail(bridge, path, fieldBase, "object", raw, `each entry under ${fieldBase} must be an object with 'path', 'format', and 'map'`);
|
|
46
|
+
}
|
|
47
|
+
const obj = raw;
|
|
48
|
+
if (typeof obj.path !== "string" || !obj.path) {
|
|
49
|
+
fail(bridge, path, `${fieldBase}.path`, "non-empty string", obj.path, `${fieldBase}.path must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof obj.format !== "string") {
|
|
52
|
+
fail(bridge, path, `${fieldBase}.format`, "string", obj.format, `${fieldBase}.format must be one of: ${VALID_FORMATS.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
if (!VALID_FORMATS.includes(obj.format)) {
|
|
55
|
+
fail(bridge, path, `${fieldBase}.format`, VALID_FORMATS.join(" | "), obj.format, `unsupported format "${obj.format}"; supported: ${VALID_FORMATS.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof obj.map !== "object" || obj.map === null || Array.isArray(obj.map)) {
|
|
58
|
+
fail(bridge, path, `${fieldBase}.map`, "object", obj.map, `${fieldBase}.map must be an object of BridgeMemory-field → mapping expression`);
|
|
59
|
+
}
|
|
60
|
+
const mapIn = obj.map;
|
|
61
|
+
const map = {};
|
|
62
|
+
for (const [k, v] of Object.entries(mapIn)) {
|
|
63
|
+
if (typeof v !== "string") {
|
|
64
|
+
fail(bridge, path, `${fieldBase}.map.${k}`, "string expression", v, `map entries must be strings; got ${typeof v} for ${k}`);
|
|
65
|
+
}
|
|
66
|
+
map[k] = v;
|
|
67
|
+
}
|
|
68
|
+
const out = {
|
|
69
|
+
path: obj.path,
|
|
70
|
+
format: obj.format,
|
|
71
|
+
map,
|
|
72
|
+
};
|
|
73
|
+
if (typeof obj.when === "string")
|
|
74
|
+
out.when = obj.when;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
export async function loadYamlDescriptor(yamlPath) {
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = await fsp.readFile(yamlPath, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
failLoad(yamlPath, "(file)", "readable file", err?.code ?? "ENOENT", `could not read YAML file: ${err?.message ?? err}`);
|
|
84
|
+
}
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = yaml.load(raw);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
failLoad(yamlPath, "(yaml)", "valid YAML", err?.name ?? "parse error", `YAML parse failed: ${err?.message ?? err}`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
93
|
+
failLoad(yamlPath, "(root)", "mapping", parsed, "descriptor must be a YAML mapping at the top level");
|
|
94
|
+
}
|
|
95
|
+
const d = parsed;
|
|
96
|
+
if (typeof d.name !== "string" || !d.name) {
|
|
97
|
+
failLoad(yamlPath, "name", "non-empty string", d.name, "descriptor must have a top-level 'name' string");
|
|
98
|
+
}
|
|
99
|
+
const bridge = d.name;
|
|
100
|
+
if (d.kind !== "file") {
|
|
101
|
+
fail(bridge, yamlPath, "kind", "'file'", d.kind, "YAML descriptors have kind: file. API plugins use a TypeScript code plugin — see docs/bridges.md §6");
|
|
102
|
+
}
|
|
103
|
+
const versionRaw = d.version;
|
|
104
|
+
const version = typeof versionRaw === "number" ? versionRaw : versionRaw === undefined ? 1 : NaN;
|
|
105
|
+
if (!Number.isFinite(version)) {
|
|
106
|
+
fail(bridge, yamlPath, "version", "number", versionRaw, "version must be a number; defaulting to 1 if omitted is supported");
|
|
107
|
+
}
|
|
108
|
+
const descriptor = {
|
|
109
|
+
name: bridge,
|
|
110
|
+
version,
|
|
111
|
+
kind: "file",
|
|
112
|
+
};
|
|
113
|
+
if (typeof d.description === "string")
|
|
114
|
+
descriptor.description = d.description;
|
|
115
|
+
if (d.detect && typeof d.detect === "object") {
|
|
116
|
+
const det = d.detect;
|
|
117
|
+
const detect = {};
|
|
118
|
+
if (Array.isArray(det.anyExists)) {
|
|
119
|
+
detect.anyExists = det.anyExists.filter((x) => typeof x === "string");
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(det.allExist)) {
|
|
122
|
+
detect.allExist = det.allExist.filter((x) => typeof x === "string");
|
|
123
|
+
}
|
|
124
|
+
descriptor.detect = detect;
|
|
125
|
+
}
|
|
126
|
+
if (d.import && typeof d.import === "object") {
|
|
127
|
+
const imp = d.import;
|
|
128
|
+
if (!Array.isArray(imp.sources) || imp.sources.length === 0) {
|
|
129
|
+
fail(bridge, yamlPath, "import.sources", "non-empty array", imp.sources, "import must have a 'sources' array with at least one entry");
|
|
130
|
+
}
|
|
131
|
+
descriptor.import = {
|
|
132
|
+
sources: imp.sources.map((s, i) => normalizeSourceTarget(bridge, yamlPath, `import.sources[${i}]`, s)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (d.export && typeof d.export === "object") {
|
|
136
|
+
const exp = d.export;
|
|
137
|
+
if (!Array.isArray(exp.targets) || exp.targets.length === 0) {
|
|
138
|
+
fail(bridge, yamlPath, "export.targets", "non-empty array", exp.targets, "export must have a 'targets' array with at least one entry");
|
|
139
|
+
}
|
|
140
|
+
descriptor.export = {
|
|
141
|
+
targets: exp.targets.map((t, i) => normalizeSourceTarget(bridge, yamlPath, `export.targets[${i}]`, t)),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (!descriptor.import && !descriptor.export) {
|
|
145
|
+
fail(bridge, yamlPath, "(root)", "import or export block", "neither", "descriptor must define at least one of `import` or `export`");
|
|
146
|
+
}
|
|
147
|
+
return descriptor;
|
|
148
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge scaffolding — emits a working starter for a new bridge.
|
|
3
|
+
*
|
|
4
|
+
* From FLAIR-BRIDGES.md §2: "Scaffold & test in-tree. `flair bridge
|
|
5
|
+
* scaffold <name>` emits a working starter; `flair bridge test <name>`
|
|
6
|
+
* runs a round-trip diff." This file handles the `scaffold` half.
|
|
7
|
+
*
|
|
8
|
+
* Two shapes supported:
|
|
9
|
+
* - --file: YAML descriptor at `.flair-bridge/<name>.yaml` + fixture
|
|
10
|
+
* - --api: TS code plugin at `flair-bridge-<name>/` + package.json + fixture
|
|
11
|
+
*/
|
|
12
|
+
import { promises as fsp } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
const NAME_RE = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
15
|
+
async function exists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await fsp.stat(path);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function writeIfAbsent(path, content, force) {
|
|
25
|
+
if (!force && (await exists(path)))
|
|
26
|
+
return { wrote: false };
|
|
27
|
+
await fsp.mkdir(dirname(path), { recursive: true });
|
|
28
|
+
await fsp.writeFile(path, content, "utf-8");
|
|
29
|
+
return { wrote: true };
|
|
30
|
+
}
|
|
31
|
+
function fileDescriptorTemplate(name) {
|
|
32
|
+
return `# .flair-bridge/${name}.yaml
|
|
33
|
+
#
|
|
34
|
+
# Generated by \`flair bridge scaffold ${name} --file\`.
|
|
35
|
+
# Edit to match your foreign system, then run \`flair bridge test ${name}\`.
|
|
36
|
+
#
|
|
37
|
+
# Spec: see FLAIR-BRIDGES.md §4 (memory schema) and §5 (YAML shape).
|
|
38
|
+
name: ${name}
|
|
39
|
+
version: 1
|
|
40
|
+
kind: file
|
|
41
|
+
# description shown in 'flair bridge list':
|
|
42
|
+
description: "TODO — one-line description of what this bridge does"
|
|
43
|
+
|
|
44
|
+
# 'detect' lets auto-discovery confirm the foreign system is present.
|
|
45
|
+
# Use 'anyExists' (any one path matching is enough) or 'allExist'.
|
|
46
|
+
detect:
|
|
47
|
+
anyExists:
|
|
48
|
+
- ".example/AGENTS.md"
|
|
49
|
+
|
|
50
|
+
# Import: foreign → Flair memories.
|
|
51
|
+
import:
|
|
52
|
+
sources:
|
|
53
|
+
- path: ".example/memory/lessons.jsonl"
|
|
54
|
+
format: jsonl # jsonl | json | yaml | markdown-frontmatter
|
|
55
|
+
map:
|
|
56
|
+
# JSONPath subset: $.field, $.nested.field, $.array[*]
|
|
57
|
+
content: "$.claim"
|
|
58
|
+
subject: "$.topic"
|
|
59
|
+
tags: "$.tags"
|
|
60
|
+
foreignId: "$.id"
|
|
61
|
+
durability: "persistent"
|
|
62
|
+
source: "${name}/lessons"
|
|
63
|
+
|
|
64
|
+
# Export: Flair memories → foreign. Optional. Drop this block if read-only.
|
|
65
|
+
export:
|
|
66
|
+
targets:
|
|
67
|
+
- path: ".example/memory/lessons.jsonl"
|
|
68
|
+
format: jsonl
|
|
69
|
+
# 'when' is a boolean expression over BridgeMemory fields.
|
|
70
|
+
# Omit for "always export".
|
|
71
|
+
when: "durability in ['persistent', 'permanent']"
|
|
72
|
+
map:
|
|
73
|
+
# Right-hand side is a BridgeMemory field (or expression).
|
|
74
|
+
id: "foreignId ?? id"
|
|
75
|
+
claim: "content"
|
|
76
|
+
topic: "subject"
|
|
77
|
+
tags: "tags"
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
function fileFixtureTemplate(name) {
|
|
81
|
+
// JSONL fixture matching the default template's 'sources[0].path'.
|
|
82
|
+
return [
|
|
83
|
+
JSON.stringify({ id: "lesson-1", claim: "Always run tests before pushing.", topic: "engineering", tags: ["process", "ci"] }),
|
|
84
|
+
JSON.stringify({ id: "lesson-2", claim: "Name workarounds as workarounds.", topic: "communication", tags: ["writing"] }),
|
|
85
|
+
"",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
function apiIndexTemplate(name) {
|
|
89
|
+
return `/**
|
|
90
|
+
* flair-bridge-${name}
|
|
91
|
+
*
|
|
92
|
+
* Generated by \`flair bridge scaffold ${name} --api\`.
|
|
93
|
+
* Spec: see FLAIR-BRIDGES.md §4 (memory schema) and §6 (code plugin shape).
|
|
94
|
+
*
|
|
95
|
+
* After editing, run:
|
|
96
|
+
* flair bridge test ${name}
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
import type {
|
|
100
|
+
MemoryBridge,
|
|
101
|
+
BridgeMemory,
|
|
102
|
+
BridgeContext,
|
|
103
|
+
} from "@tpsdev-ai/flair";
|
|
104
|
+
|
|
105
|
+
export const bridge: MemoryBridge = {
|
|
106
|
+
name: "${name}",
|
|
107
|
+
version: 1,
|
|
108
|
+
kind: "api",
|
|
109
|
+
|
|
110
|
+
options: {
|
|
111
|
+
apiKey: { env: "${name.toUpperCase().replace(/-/g, "_")}_API_KEY", required: true, description: "API token" },
|
|
112
|
+
userId: { required: true, description: "Account or user identifier in the foreign system" },
|
|
113
|
+
baseUrl: { default: "https://api.example.com", description: "Foreign API base URL" },
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async *import(opts, ctx: BridgeContext): AsyncIterable<BridgeMemory> {
|
|
117
|
+
ctx.log.info("starting import", { user: String(opts.userId) });
|
|
118
|
+
const res = await ctx.fetch(\`\${String(opts.baseUrl)}/v1/memories?user=\${String(opts.userId)}\`, {
|
|
119
|
+
headers: { Authorization: \`Bearer \${String(opts.apiKey)}\` },
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
throw new Error(\`foreign API returned \${res.status}: \${await res.text()}\`);
|
|
123
|
+
}
|
|
124
|
+
const payload = await res.json() as { results?: Array<Record<string, unknown>> };
|
|
125
|
+
for (const m of payload.results ?? []) {
|
|
126
|
+
yield {
|
|
127
|
+
foreignId: String(m.id ?? ""),
|
|
128
|
+
content: String(m.content ?? ""),
|
|
129
|
+
createdAt: typeof m.created_at === "string" ? m.created_at : undefined,
|
|
130
|
+
tags: Array.isArray(m.tags) ? m.tags.map(String) : undefined,
|
|
131
|
+
source: "${name}",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async export(memories, opts, ctx): Promise<void> {
|
|
137
|
+
ctx.log.info("starting export", { user: String(opts.userId) });
|
|
138
|
+
for await (const m of memories) {
|
|
139
|
+
await ctx.fetch(\`\${String(opts.baseUrl)}/v1/memories\`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: \`Bearer \${String(opts.apiKey)}\`,
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
user: String(opts.userId),
|
|
147
|
+
content: m.content,
|
|
148
|
+
tags: m.tags ?? [],
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default bridge;
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
function apiPackageJsonTemplate(name) {
|
|
159
|
+
const pkg = {
|
|
160
|
+
name: `flair-bridge-${name}`,
|
|
161
|
+
version: "0.1.0",
|
|
162
|
+
type: "module",
|
|
163
|
+
description: `Flair bridge for ${name}`,
|
|
164
|
+
main: "index.js",
|
|
165
|
+
types: "index.d.ts",
|
|
166
|
+
scripts: {
|
|
167
|
+
build: "tsc",
|
|
168
|
+
},
|
|
169
|
+
flair: { kind: "api", version: 1 },
|
|
170
|
+
peerDependencies: {
|
|
171
|
+
"@tpsdev-ai/flair": ">=1.0.0",
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
175
|
+
}
|
|
176
|
+
function apiFixtureTemplate(name) {
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
description: `Mock responses for flair-bridge-${name}. Used by 'flair bridge test ${name}'.`,
|
|
179
|
+
import: {
|
|
180
|
+
"GET /v1/memories?user=example": {
|
|
181
|
+
status: 200,
|
|
182
|
+
body: {
|
|
183
|
+
results: [
|
|
184
|
+
{ id: "m-1", content: "First imported memory.", tags: ["example"] },
|
|
185
|
+
{ id: "m-2", content: "Second imported memory.", tags: ["example"] },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
export: {
|
|
191
|
+
"POST /v1/memories": { status: 201, body: { ok: true } },
|
|
192
|
+
},
|
|
193
|
+
}, null, 2) + "\n";
|
|
194
|
+
}
|
|
195
|
+
export async function scaffold(opts) {
|
|
196
|
+
if (!NAME_RE.test(opts.name)) {
|
|
197
|
+
throw new Error(`invalid bridge name "${opts.name}" — must match ${NAME_RE} (lowercase, digits, hyphens; start with letter).`);
|
|
198
|
+
}
|
|
199
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
200
|
+
const force = opts.force ?? false;
|
|
201
|
+
const createdFiles = [];
|
|
202
|
+
const skippedFiles = [];
|
|
203
|
+
const track = async (path, content) => {
|
|
204
|
+
const { wrote } = await writeIfAbsent(path, content, force);
|
|
205
|
+
if (wrote)
|
|
206
|
+
createdFiles.push(path);
|
|
207
|
+
else
|
|
208
|
+
skippedFiles.push(path);
|
|
209
|
+
};
|
|
210
|
+
if (opts.kind === "file") {
|
|
211
|
+
await track(join(cwd, ".flair-bridge", `${opts.name}.yaml`), fileDescriptorTemplate(opts.name));
|
|
212
|
+
await track(join(cwd, ".flair-bridge", "fixtures", `${opts.name}.fixture.jsonl`), fileFixtureTemplate(opts.name));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const pkgDir = join(cwd, `flair-bridge-${opts.name}`);
|
|
216
|
+
await track(join(pkgDir, "index.ts"), apiIndexTemplate(opts.name));
|
|
217
|
+
await track(join(pkgDir, "package.json"), apiPackageJsonTemplate(opts.name));
|
|
218
|
+
await track(join(pkgDir, "fixtures", `${opts.name}.mock.json`), apiFixtureTemplate(opts.name));
|
|
219
|
+
}
|
|
220
|
+
const summary = createdFiles.length === 0
|
|
221
|
+
? `nothing created — all files already exist. Pass --force to overwrite.`
|
|
222
|
+
: `scaffolded ${opts.kind} bridge "${opts.name}". Next: edit the descriptor, then run \`flair bridge test ${opts.name}\`.`;
|
|
223
|
+
return { createdFiles, skippedFiles, summary };
|
|
224
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Flair memory bridge plugin system.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the contract in specs/FLAIR-BRIDGES.md. These types are the
|
|
5
|
+
* public surface a bridge author (human or agent) targets.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: the YAML bridge format (shape A) also conforms to these types
|
|
8
|
+
* after parsing — the runtime normalizes YAML descriptors into the same
|
|
9
|
+
* shape a code plugin (shape B) exports.
|
|
10
|
+
*/
|
|
11
|
+
// Fields a bridge MUST NOT set — computed by Flair on ingest.
|
|
12
|
+
export const FLAIR_RESERVED_FIELDS = [
|
|
13
|
+
"contentHash",
|
|
14
|
+
"embedding",
|
|
15
|
+
"embeddingModel",
|
|
16
|
+
"retrievalCount",
|
|
17
|
+
"lastRetrieved",
|
|
18
|
+
"promotionStatus",
|
|
19
|
+
"_safetyFlags",
|
|
20
|
+
"createdBy",
|
|
21
|
+
"updatedBy",
|
|
22
|
+
"archivedBy",
|
|
23
|
+
];
|
|
24
|
+
export class BridgeRuntimeError extends Error {
|
|
25
|
+
detail;
|
|
26
|
+
constructor(detail) {
|
|
27
|
+
super(`${detail.bridge}:${detail.op} — ${detail.hint}`);
|
|
28
|
+
this.detail = detail;
|
|
29
|
+
}
|
|
30
|
+
}
|