brainjar 0.1.0 → 0.2.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 (2) hide show
  1. package/dist/brainjar.js +508 -82
  2. package/package.json +3 -3
package/dist/brainjar.js CHANGED
@@ -174,8 +174,8 @@ var init_github = __esm({
174
174
  if (this.opts.token) h.Authorization = `Bearer ${this.opts.token}`;
175
175
  return h;
176
176
  }
177
- async *paginate(path6, params) {
178
- let url = this.buildUrl(path6, params);
177
+ async *paginate(path7, params) {
178
+ let url = this.buildUrl(path7, params);
179
179
  while (url) {
180
180
  const res = await this.fetchImpl(url, { headers: this.headers() });
181
181
  if (!res.ok) {
@@ -186,9 +186,9 @@ var init_github = __esm({
186
186
  url = parseNextLink(res.headers.get("link"));
187
187
  }
188
188
  }
189
- buildUrl(path6, params) {
189
+ buildUrl(path7, params) {
190
190
  const url = new URL(
191
- `${this.baseUrl}/repos/${this.opts.owner}/${this.opts.repo}${path6}`
191
+ `${this.baseUrl}/repos/${this.opts.owner}/${this.opts.repo}${path7}`
192
192
  );
193
193
  url.searchParams.set("per_page", String(this.perPage));
194
194
  for (const [k, v] of Object.entries(params)) {
@@ -667,13 +667,13 @@ var require_textParsers = __commonJS({
667
667
  return fn(value);
668
668
  };
669
669
  }
670
- function parseBool2(value) {
670
+ function parseBool(value) {
671
671
  if (value === null) return value;
672
672
  return value === "TRUE" || value === "t" || value === "true" || value === "y" || value === "yes" || value === "on" || value === "1";
673
673
  }
674
674
  function parseBoolArray(value) {
675
675
  if (!value) return null;
676
- return array.parse(value, parseBool2);
676
+ return array.parse(value, parseBool);
677
677
  }
678
678
  function parseBaseTenInt(string) {
679
679
  return parseInt(string, 10);
@@ -808,7 +808,7 @@ var require_textParsers = __commonJS({
808
808
  register(26, parseInteger);
809
809
  register(700, parseFloat);
810
810
  register(701, parseFloat);
811
- register(16, parseBool2);
811
+ register(16, parseBool);
812
812
  register(1082, parseDate);
813
813
  register(1114, parseDate);
814
814
  register(1184, parseDate);
@@ -1105,7 +1105,7 @@ var require_binaryParsers = __commonJS({
1105
1105
  var parseText = function(value) {
1106
1106
  return value.toString("utf8");
1107
1107
  };
1108
- var parseBool2 = function(value) {
1108
+ var parseBool = function(value) {
1109
1109
  if (value === null) return null;
1110
1110
  return parseBits(value, 8) > 0;
1111
1111
  };
@@ -1117,7 +1117,7 @@ var require_binaryParsers = __commonJS({
1117
1117
  register(1700, parseNumeric);
1118
1118
  register(700, parseFloat32);
1119
1119
  register(701, parseFloat64);
1120
- register(16, parseBool2);
1120
+ register(16, parseBool);
1121
1121
  register(1114, parseDate.bind(null, false));
1122
1122
  register(1184, parseDate.bind(null, true));
1123
1123
  register(1e3, parseArray);
@@ -3671,7 +3671,7 @@ var require_split2 = __commonJS({
3671
3671
  var require_helper = __commonJS({
3672
3672
  "../../node_modules/pgpass/lib/helper.js"(exports, module) {
3673
3673
  "use strict";
3674
- var path6 = __require("path");
3674
+ var path7 = __require("path");
3675
3675
  var Stream2 = __require("stream").Stream;
3676
3676
  var split = require_split2();
3677
3677
  var util = __require("util");
@@ -3710,7 +3710,7 @@ var require_helper = __commonJS({
3710
3710
  };
3711
3711
  module.exports.getFileName = function(rawEnv) {
3712
3712
  var env = rawEnv || process.env;
3713
- var file = env.PGPASSFILE || (isWin ? path6.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path6.join(env.HOME || "./", ".pgpass"));
3713
+ var file = env.PGPASSFILE || (isWin ? path7.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path7.join(env.HOME || "./", ".pgpass"));
3714
3714
  return file;
3715
3715
  };
3716
3716
  module.exports.usePgPass = function(stats, fname) {
@@ -3842,13 +3842,13 @@ var require_helper = __commonJS({
3842
3842
  var require_lib = __commonJS({
3843
3843
  "../../node_modules/pgpass/lib/index.js"(exports, module) {
3844
3844
  "use strict";
3845
- var path6 = __require("path");
3845
+ var path7 = __require("path");
3846
3846
  var fs = __require("fs");
3847
3847
  var helper = require_helper();
3848
3848
  module.exports = function(connInfo, cb) {
3849
3849
  var file = helper.getFileName();
3850
- fs.stat(file, function(err, stat2) {
3851
- if (err || !helper.usePgPass(stat2, file)) {
3850
+ fs.stat(file, function(err, stat3) {
3851
+ if (err || !helper.usePgPass(stat3, file)) {
3852
3852
  return cb(void 0);
3853
3853
  }
3854
3854
  var st = fs.createReadStream(file);
@@ -5617,10 +5617,6 @@ function loadEnv() {
5617
5617
  }
5618
5618
  return { ...cachedFileVars, ...process.env };
5619
5619
  }
5620
- function parseBool(v) {
5621
- if (v == null) return false;
5622
- return /^(1|true|yes|on)$/i.test(v.trim());
5623
- }
5624
5620
  function loadConfig(env = loadEnv()) {
5625
5621
  return new Config(env);
5626
5622
  }
@@ -5667,7 +5663,6 @@ var init_config = __esm({
5667
5663
  };
5668
5664
  Config = class {
5669
5665
  environment;
5670
- cloudAiEnabled;
5671
5666
  databaseUrl;
5672
5667
  /** The tenant all connections are pinned to (RLS). DEFAULT_TENANT_ID in local mode. */
5673
5668
  tenantId;
@@ -5685,16 +5680,14 @@ var init_config = __esm({
5685
5680
  const tenant = env.BRAINJAR_TENANT_ID?.trim();
5686
5681
  if (!tenant) {
5687
5682
  throw new Error(
5688
- "ENVIRONMENT=saas requires BRAINJAR_TENANT_ID \u2014 every SaaS connection is tenant-scoped."
5683
+ "ENVIRONMENT=saas requires BRAINJAR_TENANT_ID: every SaaS connection is tenant-scoped."
5689
5684
  );
5690
5685
  }
5691
5686
  this.databaseUrl = url;
5692
5687
  this.tenantId = tenant;
5693
- this.cloudAiEnabled = true;
5694
5688
  } else {
5695
5689
  this.databaseUrl = env.DATABASE_URL?.trim() || DEFAULT_DATABASE_URL;
5696
5690
  this.tenantId = DEFAULT_TENANT_ID;
5697
- this.cloudAiEnabled = parseBool(env.BRAINJAR_CLOUD_AI);
5698
5691
  }
5699
5692
  }
5700
5693
  /** Returns a secret, or throws an actionable error naming the env var and its purpose. */
@@ -5705,17 +5698,11 @@ var init_config = __esm({
5705
5698
  if (!value2) {
5706
5699
  throw new MissingSecretError(
5707
5700
  key,
5708
- `Missing ${CORPORATE_API_KEY_ENV}. In SaaS mode, ${spec.what} is billed to the central corporate account \u2014 set ${CORPORATE_API_KEY_ENV} in the backend environment.`
5701
+ `Missing ${CORPORATE_API_KEY_ENV}. In SaaS mode, ${spec.what} is billed to the central corporate account: set ${CORPORATE_API_KEY_ENV} in the backend environment.`
5709
5702
  );
5710
5703
  }
5711
5704
  return value2;
5712
5705
  }
5713
- if (spec.cloudAi && !this.cloudAiEnabled) {
5714
- throw new MissingSecretError(
5715
- key,
5716
- `Cloud AI is disabled (BrainJar is local-first by default). To use ${spec.what}, set BRAINJAR_CLOUD_AI=true and ${spec.env}.`
5717
- );
5718
- }
5719
5706
  const value = this.#env[spec.env]?.trim();
5720
5707
  if (!value) {
5721
5708
  throw new MissingSecretError(
@@ -5738,7 +5725,6 @@ var init_config = __esm({
5738
5725
  return {
5739
5726
  environment: this.environment,
5740
5727
  tenantId: this.tenantId,
5741
- cloudAiEnabled: this.cloudAiEnabled,
5742
5728
  databaseUrl: redactUrlPassword(this.databaseUrl)
5743
5729
  };
5744
5730
  }
@@ -5753,12 +5739,14 @@ var init_config = __esm({
5753
5739
  });
5754
5740
 
5755
5741
  // ../../packages/shared/src/index.ts
5742
+ var PROSE_STYLE;
5756
5743
  var init_src = __esm({
5757
5744
  "../../packages/shared/src/index.ts"() {
5758
5745
  "use strict";
5759
5746
  init_memory();
5760
5747
  init_confidence();
5761
5748
  init_config();
5749
+ PROSE_STYLE = "Never use em dashes (\u2014) or en dashes (\u2013) anywhere; use commas, colons, periods, or parentheses instead.";
5762
5750
  }
5763
5751
  });
5764
5752
 
@@ -5917,6 +5905,34 @@ var init_repository = __esm({
5917
5905
  [this.brainId]
5918
5906
  )).rows;
5919
5907
  }
5908
+ /**
5909
+ * Verified facts whose verified_scan evidence points at any of these repo-relative files —
5910
+ * the re-verification trigger: a commit touched their proof, so their status may be stale.
5911
+ */
5912
+ async verifiedObjectsTouchingFiles(paths) {
5913
+ if (paths.length === 0) return [];
5914
+ return (await this.pool.query(
5915
+ `SELECT DISTINCT ${MO_COLUMNS}, ${EV_AGG} FROM memory_object
5916
+ WHERE brain_id = $1 AND status = 'active'
5917
+ AND EXISTS (
5918
+ SELECT 1 FROM evidence_link ev
5919
+ WHERE ev.memory_object_id = memory_object.id
5920
+ AND ev.artifact_kind = 'verified_scan'
5921
+ AND EXISTS (
5922
+ SELECT 1 FROM unnest($2::text[]) AS p(path)
5923
+ WHERE ev.artifact_uri LIKE '%/' || p.path
5924
+ )
5925
+ )`,
5926
+ [this.brainId, paths]
5927
+ )).rows;
5928
+ }
5929
+ /** Replace a source's config (e.g. the GitHub repo/token a Brain reads from). */
5930
+ async setSourceConfig(id, config) {
5931
+ await this.pool.query(
5932
+ "UPDATE source SET config = $3 WHERE id = $1 AND brain_id = $2",
5933
+ [id, this.brainId, JSON.stringify(config)]
5934
+ );
5935
+ }
5920
5936
  async getSource(id) {
5921
5937
  return (await this.pool.query(
5922
5938
  "SELECT * FROM source WHERE id = $1 AND brain_id = $2",
@@ -6144,6 +6160,18 @@ var init_repository = __esm({
6144
6160
  [this.brainId, startIso, endIso, limit]
6145
6161
  )).rows;
6146
6162
  }
6163
+ /**
6164
+ * The newest substantive objects ("what are the latest ...?"). Entities are timeless
6165
+ * structure, not news, so they are excluded along with summaries.
6166
+ */
6167
+ async newestObjects(limit = 8) {
6168
+ return (await this.pool.query(
6169
+ `SELECT ${MO_COLUMNS}, ${EV_AGG} FROM memory_object
6170
+ WHERE brain_id = $1 AND type NOT IN ('summary', 'entity')
6171
+ ORDER BY occurred_at DESC LIMIT $2`,
6172
+ [this.brainId, limit]
6173
+ )).rows;
6174
+ }
6147
6175
  /** The daily_log (chronology rollup) for a given day, if one exists (one per active day). */
6148
6176
  async findDailyLog(date) {
6149
6177
  return (await this.pool.query(
@@ -6602,7 +6630,7 @@ var init_auth = __esm({
6602
6630
  "../../packages/db/src/auth.ts"() {
6603
6631
  "use strict";
6604
6632
  init_pool();
6605
- DEFAULT_TTL_HOURS = 24 * 7;
6633
+ DEFAULT_TTL_HOURS = 24 * 90;
6606
6634
  }
6607
6635
  });
6608
6636
 
@@ -6653,10 +6681,24 @@ async function resolveSetting(key, envValue, pool2 = getPool()) {
6653
6681
  return void 0;
6654
6682
  }
6655
6683
  }
6656
- var APP_SETTING_KEYS;
6684
+ async function resolveAnthropicApiKey(envKey, pool2 = getPool()) {
6685
+ const fromEnv = envKey?.trim();
6686
+ if (fromEnv) return fromEnv;
6687
+ try {
6688
+ const r = await pool2.query(
6689
+ "SELECT value FROM app_setting WHERE key = $1",
6690
+ [ANTHROPIC_KEY_SETTING]
6691
+ );
6692
+ return r.rows[0]?.value?.trim() || null;
6693
+ } catch {
6694
+ return null;
6695
+ }
6696
+ }
6697
+ var APP_SETTING_KEYS, ANTHROPIC_KEY_SETTING;
6657
6698
  var init_settings = __esm({
6658
6699
  "../../packages/db/src/settings.ts"() {
6659
6700
  "use strict";
6701
+ init_src();
6660
6702
  init_pool();
6661
6703
  APP_SETTING_KEYS = [
6662
6704
  "capture_mode",
@@ -6670,6 +6712,23 @@ var init_settings = __esm({
6670
6712
  "public_url"
6671
6713
  // this server's address as clients reach it (wizard command, MCP config)
6672
6714
  ];
6715
+ ANTHROPIC_KEY_SETTING = "anthropic_api_key";
6716
+ }
6717
+ });
6718
+
6719
+ // ../../packages/db/src/notifications.ts
6720
+ var init_notifications = __esm({
6721
+ "../../packages/db/src/notifications.ts"() {
6722
+ "use strict";
6723
+ init_pool();
6724
+ }
6725
+ });
6726
+
6727
+ // ../../packages/db/src/conversations.ts
6728
+ var init_conversations = __esm({
6729
+ "../../packages/db/src/conversations.ts"() {
6730
+ "use strict";
6731
+ init_pool();
6673
6732
  }
6674
6733
  });
6675
6734
 
@@ -6685,6 +6744,8 @@ var init_src2 = __esm({
6685
6744
  init_team();
6686
6745
  init_rbac();
6687
6746
  init_settings();
6747
+ init_notifications();
6748
+ init_conversations();
6688
6749
  }
6689
6750
  });
6690
6751
 
@@ -6775,17 +6836,103 @@ var init_raw_store = __esm({
6775
6836
  async listForExtraction(brainId, limit = 500) {
6776
6837
  return (await this.pool.query(
6777
6838
  `SELECT * FROM raw_artifact
6778
- WHERE brain_id = $1 AND extracted_at IS NULL
6839
+ WHERE brain_id = $1 AND extracted_at IS NULL AND kind <> 'source_file'
6840
+ ORDER BY first_seen_at LIMIT $2`,
6841
+ [brainId, limit]
6842
+ )).rows;
6843
+ }
6844
+ /**
6845
+ * Deep-scan queue: source files awaiting synthesis. Covers both fresh uploads (pending) and
6846
+ * files stored while no AI key existed ('raw-source'), so adding a key later and re-running
6847
+ * the scan picks them up.
6848
+ */
6849
+ /** How many shipped source files still await synthesis (pending or stored without a key). */
6850
+ async sourceScanCount(brainId) {
6851
+ return (await this.pool.query(
6852
+ `SELECT count(*)::int AS n FROM raw_artifact
6853
+ WHERE brain_id = $1 AND kind = 'source_file'
6854
+ AND (extracted_at IS NULL OR extracted_by = 'raw-source')`,
6855
+ [brainId]
6856
+ )).rows[0]?.n ?? 0;
6857
+ }
6858
+ async listSourceFilesForScan(brainId, limit = 500) {
6859
+ return (await this.pool.query(
6860
+ `SELECT * FROM raw_artifact
6861
+ WHERE brain_id = $1 AND kind = 'source_file'
6862
+ AND (extracted_at IS NULL OR extracted_by = 'raw-source')
6779
6863
  ORDER BY first_seen_at LIMIT $2`,
6780
6864
  [brainId, limit]
6781
6865
  )).rows;
6782
6866
  }
6783
- async markExtracted(id) {
6867
+ async markExtracted(id, by) {
6784
6868
  await this.pool.query(
6785
- "UPDATE raw_artifact SET extracted_at = now() WHERE id = $1",
6786
- [id]
6869
+ "UPDATE raw_artifact SET extracted_at = now(), extracted_by = $2 WHERE id = $1",
6870
+ [id, by]
6787
6871
  );
6788
6872
  }
6873
+ /**
6874
+ * The two numbers the Re-synthesize UI keys on. `pending` is the NORMAL pipeline (fresh
6875
+ * artifacts not yet processed — never a reason to offer re-synthesis); `heuristic` is the
6876
+ * backlog captured without AI, the only thing the button acts on.
6877
+ */
6878
+ async extractionState(brainId) {
6879
+ const r = await this.pool.query(
6880
+ `SELECT
6881
+ count(*) FILTER (WHERE extracted_at IS NULL) AS pending,
6882
+ count(*) FILTER (WHERE extracted_by = 'heuristic') AS heuristic
6883
+ FROM raw_artifact WHERE brain_id = $1`,
6884
+ [brainId]
6885
+ );
6886
+ return {
6887
+ pending: Number(r.rows[0]?.pending ?? 0),
6888
+ heuristic: Number(r.rows[0]?.heuristic ?? 0)
6889
+ };
6890
+ }
6891
+ /**
6892
+ * Prepare heuristically-extracted artifacts for AI re-extraction, atomically:
6893
+ * their derived non-entity objects are deleted (evidence/object links cascade), stale
6894
+ * entity evidence pointing at these artifacts is dropped (re-extraction re-adds it —
6895
+ * entities themselves survive, resolveEntity dedupes them on the re-run), and the
6896
+ * artifacts return to the pipeline (extracted_at/by cleared). Returns how many artifacts
6897
+ * were re-queued — 0 means nothing to do, which is also what makes a double click a no-op.
6898
+ */
6899
+ async clearForResynthesis(brainId) {
6900
+ const client = await this.pool.connect();
6901
+ try {
6902
+ await client.query("BEGIN");
6903
+ const ids = (await client.query(
6904
+ `SELECT id FROM raw_artifact
6905
+ WHERE brain_id = $1 AND extracted_by = 'heuristic' FOR UPDATE`,
6906
+ [brainId]
6907
+ )).rows.map((r) => r.id);
6908
+ if (ids.length === 0) {
6909
+ await client.query("ROLLBACK");
6910
+ return 0;
6911
+ }
6912
+ await client.query(
6913
+ `DELETE FROM memory_object WHERE type <> 'entity' AND id IN (
6914
+ SELECT memory_object_id FROM evidence_link
6915
+ WHERE raw_artifact_id = ANY($1::uuid[]))`,
6916
+ [ids]
6917
+ );
6918
+ await client.query(
6919
+ "DELETE FROM evidence_link WHERE raw_artifact_id = ANY($1::uuid[])",
6920
+ [ids]
6921
+ );
6922
+ await client.query(
6923
+ `UPDATE raw_artifact SET extracted_at = NULL, extracted_by = NULL
6924
+ WHERE id = ANY($1::uuid[])`,
6925
+ [ids]
6926
+ );
6927
+ await client.query("COMMIT");
6928
+ return ids.length;
6929
+ } catch (e) {
6930
+ await client.query("ROLLBACK");
6931
+ throw e;
6932
+ } finally {
6933
+ client.release();
6934
+ }
6935
+ }
6789
6936
  };
6790
6937
  }
6791
6938
  });
@@ -8261,12 +8408,12 @@ var init_path = __esm({
8261
8408
  "../../node_modules/@anthropic-ai/sdk/internal/utils/path.mjs"() {
8262
8409
  init_error();
8263
8410
  EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
8264
- createPathTagFunction = (pathEncoder = encodeURIPath) => function path6(statics, ...params) {
8411
+ createPathTagFunction = (pathEncoder = encodeURIPath) => function path7(statics, ...params) {
8265
8412
  if (statics.length === 1)
8266
8413
  return statics[0];
8267
8414
  let postPath = false;
8268
8415
  const invalidSegments = [];
8269
- const path7 = statics.reduce((previousValue, currentValue, index) => {
8416
+ const path8 = statics.reduce((previousValue, currentValue, index) => {
8270
8417
  if (/[?#]/.test(currentValue)) {
8271
8418
  postPath = true;
8272
8419
  }
@@ -8283,7 +8430,7 @@ var init_path = __esm({
8283
8430
  }
8284
8431
  return previousValue + currentValue + (index === params.length ? "" : encoded);
8285
8432
  }, "");
8286
- const pathOnly = path7.split(/[?#]/, 1)[0];
8433
+ const pathOnly = path8.split(/[?#]/, 1)[0];
8287
8434
  const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
8288
8435
  let match;
8289
8436
  while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
@@ -8304,10 +8451,10 @@ var init_path = __esm({
8304
8451
  }, "");
8305
8452
  throw new AnthropicError(`Path parameters result in path with invalid segments:
8306
8453
  ${invalidSegments.map((e) => e.error).join("\n")}
8307
- ${path7}
8454
+ ${path8}
8308
8455
  ${underline}`);
8309
8456
  }
8310
- return path7;
8457
+ return path8;
8311
8458
  };
8312
8459
  path2 = /* @__PURE__ */ createPathTagFunction(encodeURIPath);
8313
8460
  }
@@ -11293,9 +11440,9 @@ var init_client = __esm({
11293
11440
  makeStatusError(status, error, message, headers) {
11294
11441
  return APIError.generate(status, error, message, headers);
11295
11442
  }
11296
- buildURL(path6, query, defaultBaseURL) {
11443
+ buildURL(path7, query, defaultBaseURL) {
11297
11444
  const baseURL = !__classPrivateFieldGet(this, _BaseAnthropic_instances, "m", _BaseAnthropic_baseURLOverridden).call(this) && defaultBaseURL || this.baseURL;
11298
- const url = isAbsoluteURL(path6) ? new URL(path6) : new URL(baseURL + (baseURL.endsWith("/") && path6.startsWith("/") ? path6.slice(1) : path6));
11445
+ const url = isAbsoluteURL(path7) ? new URL(path7) : new URL(baseURL + (baseURL.endsWith("/") && path7.startsWith("/") ? path7.slice(1) : path7));
11299
11446
  const defaultQuery = this.defaultQuery();
11300
11447
  if (!isEmptyObj(defaultQuery)) {
11301
11448
  query = { ...defaultQuery, ...query };
@@ -11326,24 +11473,24 @@ var init_client = __esm({
11326
11473
  */
11327
11474
  async prepareRequest(request, { url, options }) {
11328
11475
  }
11329
- get(path6, opts) {
11330
- return this.methodRequest("get", path6, opts);
11476
+ get(path7, opts) {
11477
+ return this.methodRequest("get", path7, opts);
11331
11478
  }
11332
- post(path6, opts) {
11333
- return this.methodRequest("post", path6, opts);
11479
+ post(path7, opts) {
11480
+ return this.methodRequest("post", path7, opts);
11334
11481
  }
11335
- patch(path6, opts) {
11336
- return this.methodRequest("patch", path6, opts);
11482
+ patch(path7, opts) {
11483
+ return this.methodRequest("patch", path7, opts);
11337
11484
  }
11338
- put(path6, opts) {
11339
- return this.methodRequest("put", path6, opts);
11485
+ put(path7, opts) {
11486
+ return this.methodRequest("put", path7, opts);
11340
11487
  }
11341
- delete(path6, opts) {
11342
- return this.methodRequest("delete", path6, opts);
11488
+ delete(path7, opts) {
11489
+ return this.methodRequest("delete", path7, opts);
11343
11490
  }
11344
- methodRequest(method, path6, opts) {
11491
+ methodRequest(method, path7, opts) {
11345
11492
  return this.request(Promise.resolve(opts).then((opts2) => {
11346
- return { method, path: path6, ...opts2 };
11493
+ return { method, path: path7, ...opts2 };
11347
11494
  }));
11348
11495
  }
11349
11496
  request(options, remainingRetries = null) {
@@ -11447,8 +11594,8 @@ var init_client = __esm({
11447
11594
  }));
11448
11595
  return { response, options, controller, requestLogID, retryOfRequestLogID, startTime };
11449
11596
  }
11450
- getAPIList(path6, Page2, opts) {
11451
- return this.requestAPIList(Page2, { method: "get", path: path6, ...opts });
11597
+ getAPIList(path7, Page2, opts) {
11598
+ return this.requestAPIList(Page2, { method: "get", path: path7, ...opts });
11452
11599
  }
11453
11600
  requestAPIList(Page2, options) {
11454
11601
  const request = this.makeRequest(options, null, void 0);
@@ -11535,8 +11682,8 @@ var init_client = __esm({
11535
11682
  }
11536
11683
  async buildRequest(inputOptions, { retryCount = 0 } = {}) {
11537
11684
  const options = { ...inputOptions };
11538
- const { method, path: path6, query, defaultBaseURL } = options;
11539
- const url = this.buildURL(path6, query, defaultBaseURL);
11685
+ const { method, path: path7, query, defaultBaseURL } = options;
11686
+ const url = this.buildURL(path7, query, defaultBaseURL);
11540
11687
  if ("timeout" in options)
11541
11688
  validatePositiveInteger("timeout", options.timeout);
11542
11689
  options.timeout = options.timeout ?? this.timeout;
@@ -11652,11 +11799,11 @@ var init_sdk = __esm({
11652
11799
 
11653
11800
  // ../../packages/ingest/src/llm.ts
11654
11801
  function rationaleModelId(env = process.env) {
11655
- return env.BRAINJAR_RATIONALE_MODEL?.trim() || "claude-opus-4-8";
11802
+ return env.BRAINJAR_RATIONALE_MODEL?.trim() || "claude-opus-5";
11656
11803
  }
11657
- function anthropicClientFromConfig(config, model) {
11804
+ function anthropicClient(apiKey, model) {
11658
11805
  return new AnthropicClient({
11659
- apiKey: config.requireSecret("anthropicApiKey"),
11806
+ apiKey,
11660
11807
  model: model?.trim() || rationaleModelId()
11661
11808
  });
11662
11809
  }
@@ -11685,6 +11832,24 @@ var init_llm = __esm({
11685
11832
  });
11686
11833
  return res.content.map((b) => b.type === "text" ? b.text : "").join("").trim();
11687
11834
  }
11835
+ /** Same completion, but yielded as text deltas for surfaces that render tokens live (Ask chat). */
11836
+ async *completeStream({
11837
+ system,
11838
+ prompt,
11839
+ maxTokens = 1024
11840
+ }) {
11841
+ const stream = this.client.messages.stream({
11842
+ model: this.model,
11843
+ max_tokens: maxTokens,
11844
+ ...system ? { system } : {},
11845
+ messages: [{ role: "user", content: prompt }]
11846
+ });
11847
+ for await (const event of stream) {
11848
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
11849
+ yield event.delta.text;
11850
+ }
11851
+ }
11852
+ }
11688
11853
  };
11689
11854
  }
11690
11855
  });
@@ -11694,12 +11859,14 @@ var SYSTEM;
11694
11859
  var init_rationale_extractor = __esm({
11695
11860
  "../../packages/ingest/src/rationale-extractor.ts"() {
11696
11861
  "use strict";
11862
+ init_src();
11697
11863
  init_heuristic_extractor();
11698
11864
  SYSTEM = [
11699
11865
  "You extract the WHY behind a software artifact for a memory system.",
11700
- "Given an artifact (commit, PR, review comment, or agent session), explain in 1-3 sentences why it exists",
11701
- "\u2014 the decision, intent, or business reason it reflects. Infer from the evidence; do not invent specifics.",
11702
- "Respond with only the rationale sentences. No preamble, no headings, no quotes."
11866
+ "Given an artifact (commit, PR, review comment, or agent session), explain in 1-3 sentences why it exists:",
11867
+ "the decision, intent, or business reason it reflects. Infer from the evidence; do not invent specifics.",
11868
+ "Respond with only the rationale sentences. No preamble, no headings, no quotes.",
11869
+ PROSE_STYLE
11703
11870
  ].join(" ");
11704
11871
  }
11705
11872
  });
@@ -11719,6 +11886,14 @@ var init_impact = __esm({
11719
11886
  }
11720
11887
  });
11721
11888
 
11889
+ // ../../packages/ingest/src/commit-links.ts
11890
+ var init_commit_links = __esm({
11891
+ "../../packages/ingest/src/commit-links.ts"() {
11892
+ "use strict";
11893
+ init_src2();
11894
+ }
11895
+ });
11896
+
11722
11897
  // ../../packages/ingest/src/extraction.ts
11723
11898
  var init_extraction = __esm({
11724
11899
  "../../packages/ingest/src/extraction.ts"() {
@@ -11728,6 +11903,9 @@ var init_extraction = __esm({
11728
11903
  init_raw_store();
11729
11904
  init_signal();
11730
11905
  init_impact();
11906
+ init_commit_links();
11907
+ init_entities();
11908
+ init_rationale_extractor();
11731
11909
  }
11732
11910
  });
11733
11911
 
@@ -11797,6 +11975,9 @@ var init_structure = __esm({
11797
11975
  SKIP_DIRS = /* @__PURE__ */ new Set([
11798
11976
  "node_modules",
11799
11977
  ".git",
11978
+ // Parallel agent worktrees live inside the repo; scanning them creates duplicate entities
11979
+ // with .claude/worktrees/... paths that pollute retrieval.
11980
+ ".claude",
11800
11981
  ".next",
11801
11982
  "dist",
11802
11983
  "build",
@@ -11814,6 +11995,7 @@ var init_product_facts = __esm({
11814
11995
  "use strict";
11815
11996
  init_src2();
11816
11997
  init_entities();
11998
+ init_src();
11817
11999
  PRODUCT_FACTS_SYSTEM = [
11818
12000
  "You read a raw coding-agent session and extract durable PRODUCT facts for a documentation system.",
11819
12001
  "Output ONLY a JSON array (no prose, no code fences). Each item:",
@@ -11822,8 +12004,9 @@ var init_product_facts = __esm({
11822
12004
  "A 'feature' is user- or developer-facing capability (a button, a page, an endpoint, a command).",
11823
12005
  "A 'decision' is an architectural/design choice with a reason.",
11824
12006
  "Capture what was BUILT or CHANGED, where it lives (files/symbols), how it behaves, why, and any",
11825
- "configuration it exposes. Prefer a few high-signal facts over many trivial ones. Invent nothing \u2014",
11826
- "if the session only debugged or explored, return []."
12007
+ "configuration it exposes. Prefer a few high-signal facts over many trivial ones. Invent nothing:",
12008
+ "if the session only debugged or explored, return [].",
12009
+ PROSE_STYLE
11827
12010
  ].join(" ");
11828
12011
  }
11829
12012
  });
@@ -11840,24 +12023,18 @@ var CONSOLIDATE_SYSTEM;
11840
12023
  var init_compress = __esm({
11841
12024
  "../../packages/ingest/src/compress.ts"() {
11842
12025
  "use strict";
12026
+ init_src();
11843
12027
  CONSOLIDATE_SYSTEM = [
11844
12028
  "You compress a raw multi-turn coding-agent session for a memory system.",
11845
12029
  "Produce a single high-density summary with exactly three sections: Intent, Failed Attempts, Resolution.",
11846
12030
  "Preserve 100% of the architectural reasoning and the chronological order of events.",
11847
12031
  "Keep every distinct error message (once) and every rejected approach with why it failed.",
11848
- "Drop all pleasantries, tool chatter, and restated code. Target at most half the input length."
12032
+ "Drop all pleasantries, tool chatter, and restated code. Target at most half the input length.",
12033
+ PROSE_STYLE
11849
12034
  ].join(" ");
11850
12035
  }
11851
12036
  });
11852
12037
 
11853
- // ../../packages/ingest/src/commit-links.ts
11854
- var init_commit_links = __esm({
11855
- "../../packages/ingest/src/commit-links.ts"() {
11856
- "use strict";
11857
- init_src2();
11858
- }
11859
- });
11860
-
11861
12038
  // ../../packages/ingest/src/transcript.ts
11862
12039
  var init_transcript = __esm({
11863
12040
  "../../packages/ingest/src/transcript.ts"() {
@@ -12643,6 +12820,7 @@ var init_doku = __esm({
12643
12820
  "../../packages/ingest/src/doku.ts"() {
12644
12821
  "use strict";
12645
12822
  init_src2();
12823
+ init_src();
12646
12824
  init_doku_model();
12647
12825
  DOKU_FILENAME = "BRAINJAR_DOKU.md";
12648
12826
  DOKU_DIR = "brainjar-doku";
@@ -12655,14 +12833,83 @@ var init_doku = __esm({
12655
12833
  );
12656
12834
  isModule = (o) => o.type === "entity" && o.body.kind === "module";
12657
12835
  moduleName = (o) => String(o.body.name ?? o.title);
12658
- isRepoPath = (name) => !/^([a-zA-Z]:[/\\]|[/\\])/.test(name) && !name.includes("node_modules");
12836
+ isRepoPath = (name) => !/^([a-zA-Z]:[/\\]|[/\\])/.test(name) && !name.includes("node_modules") && !name.includes(".claude/");
12659
12837
  normPath = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
12660
12838
  NARRATIVE_SYSTEM = [
12661
12839
  "You write the introduction for one feature's documentation page, for a mixed audience",
12662
12840
  "(developers, product managers, leadership). You are given the page's structured facts as",
12663
12841
  "markdown. Respond with 1-3 short plain-text paragraphs that explain the feature naturally:",
12664
12842
  "what it is, why it exists, how it evolved. Keep every fact; invent nothing. No headings,",
12665
- "no lists, no markdown syntax \u2014 prose only."
12843
+ "no lists, no markdown syntax, prose only.",
12844
+ PROSE_STYLE
12845
+ ].join(" ");
12846
+ }
12847
+ });
12848
+
12849
+ // ../../packages/ingest/src/deepscan.ts
12850
+ var SOURCE_FACTS_SYSTEM;
12851
+ var init_deepscan = __esm({
12852
+ "../../packages/ingest/src/deepscan.ts"() {
12853
+ "use strict";
12854
+ init_src2();
12855
+ init_src();
12856
+ init_raw_store();
12857
+ init_product_facts();
12858
+ init_doku();
12859
+ SOURCE_FACTS_SYSTEM = [
12860
+ "You read SOURCE FILES of a software project (each introduced by a '=== path ===' header) and extract durable PRODUCT facts for a documentation system.",
12861
+ "Output ONLY a JSON array (no prose, no code fences). Each item:",
12862
+ '{ "kind": "feature"|"decision", "title": string, "summary": string,',
12863
+ ' "where": string[], "behavior"?: string, "rationale"?: string, "config"?: string[] }.',
12864
+ "A 'feature' is user- or developer-facing capability (a page, an endpoint, a command, a job).",
12865
+ "A 'decision' is an architectural/design choice whose reason is visible in the code or comments.",
12866
+ "Describe what the code DOES today, where it lives (use the given paths in 'where'), how it",
12867
+ "behaves, why it is built that way when stated, and any configuration it exposes.",
12868
+ "Prefer a few high-signal facts per batch over many trivial ones. Invent nothing:",
12869
+ "if the files carry no product knowledge (pure plumbing), return [].",
12870
+ PROSE_STYLE
12871
+ ].join(" ");
12872
+ }
12873
+ });
12874
+
12875
+ // ../../packages/ingest/src/github-app.ts
12876
+ var init_github_app = __esm({
12877
+ "../../packages/ingest/src/github-app.ts"() {
12878
+ "use strict";
12879
+ }
12880
+ });
12881
+
12882
+ // ../../packages/ingest/src/repo-reader.ts
12883
+ var init_repo_reader = __esm({
12884
+ "../../packages/ingest/src/repo-reader.ts"() {
12885
+ "use strict";
12886
+ init_structure();
12887
+ init_github_app();
12888
+ }
12889
+ });
12890
+
12891
+ // ../../packages/ingest/src/verify-agent.ts
12892
+ var VERIFY_SYSTEM;
12893
+ var init_verify_agent = __esm({
12894
+ "../../packages/ingest/src/verify-agent.ts"() {
12895
+ "use strict";
12896
+ init_sdk();
12897
+ init_src2();
12898
+ init_src();
12899
+ init_product_facts();
12900
+ init_doku();
12901
+ init_llm();
12902
+ VERIFY_SYSTEM = [
12903
+ "You are BrainJar's verification agent. You read a software repository and record durable product knowledge, with one iron rule: prose is never truth, code is.",
12904
+ "Documentation (READMEs, docs) gives you HYPOTHESES. Before recording any fact, read the implementing code with read_file and check it.",
12905
+ "Record via record_fact with the honest verification status:",
12906
+ "- confirmed: you read the implementation and it does this today.",
12907
+ "- contradicted: the docs claim one thing, the code does another. Record what the CODE does as the fact; put the outdated claim in `contradicts`.",
12908
+ "- documented-only: claimed in prose, but you could not find it in the code. Record it as documented intent, phrase the summary accordingly.",
12909
+ "Always fill `where` with the repo paths you actually read for this fact.",
12910
+ "Work highest-signal first: what the product is, its main capabilities, its architecture decisions. A few verified facts beat many shallow ones.",
12911
+ "When you are done (or told the budget is exhausted), reply with a short plain-text summary instead of more tool calls.",
12912
+ PROSE_STYLE
12666
12913
  ].join(" ");
12667
12914
  }
12668
12915
  });
@@ -12711,6 +12958,10 @@ var init_src3 = __esm({
12711
12958
  init_summaries();
12712
12959
  init_doku();
12713
12960
  init_doku_model();
12961
+ init_deepscan();
12962
+ init_repo_reader();
12963
+ init_github_app();
12964
+ init_verify_agent();
12714
12965
  init_scheduler();
12715
12966
  registerConnector(
12716
12967
  "github",
@@ -12883,6 +13134,7 @@ async function init(argv) {
12883
13134
  `
12884
13135
  \u2705 Brain "${name}" is live \u2014 ${final.objects} memory objects.
12885
13136
  Open the web app and visit Docs. Approve the hook in Claude Code when prompted.
13137
+ Optional: \`npx brainjar deepscan\` reads the repo's key files so the docs start full.
12886
13138
  `
12887
13139
  );
12888
13140
  return 0;
@@ -13226,6 +13478,174 @@ var init_doctor = __esm({
13226
13478
  }
13227
13479
  });
13228
13480
 
13481
+ // ../../packages/cli/src/deepscan.ts
13482
+ var deepscan_exports = {};
13483
+ __export(deepscan_exports, {
13484
+ deepscanMain: () => deepscanMain,
13485
+ selectDeepScanFiles: () => selectDeepScanFiles
13486
+ });
13487
+ import { readdir as readdir3, readFile as readFile6, stat as stat2 } from "node:fs/promises";
13488
+ import path6 from "node:path";
13489
+ function selectDeepScanFiles(candidates, graph, maxFiles = MAX_FILES) {
13490
+ const picked = [];
13491
+ const has = /* @__PURE__ */ new Set();
13492
+ const take = (p) => {
13493
+ if (!has.has(p) && picked.length < maxFiles) {
13494
+ has.add(p);
13495
+ picked.push(p);
13496
+ }
13497
+ };
13498
+ for (const bucket of BUCKET_CAPS) {
13499
+ let n = 0;
13500
+ const ordered = [...candidates].sort().reverse();
13501
+ for (const p of ordered) {
13502
+ if (n >= bucket.cap) break;
13503
+ if (bucket.test(p) && !has.has(p)) {
13504
+ take(p);
13505
+ n++;
13506
+ }
13507
+ }
13508
+ }
13509
+ const inDegree = /* @__PURE__ */ new Map();
13510
+ for (const e of graph.edges)
13511
+ inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
13512
+ const central = [...inDegree.entries()].sort((a, b) => b[1] - a[1]).map(([p]) => p);
13513
+ for (const p of central) {
13514
+ if (picked.length >= maxFiles) break;
13515
+ take(p);
13516
+ }
13517
+ return picked;
13518
+ }
13519
+ async function walkCandidates(root) {
13520
+ const entries = await readdir3(root, { recursive: true, withFileTypes: true });
13521
+ const out = [];
13522
+ for (const e of entries) {
13523
+ if (!e.isFile()) continue;
13524
+ const parent = e.parentPath ?? e.path ?? root;
13525
+ const rel = path6.relative(root, path6.join(parent, e.name)).split(path6.sep).join("/");
13526
+ if (rel.split("/").some((seg) => SKIP_DIRS.has(seg))) continue;
13527
+ out.push(rel);
13528
+ }
13529
+ return out;
13530
+ }
13531
+ async function deepscanMain() {
13532
+ const root = await repoRoot();
13533
+ const project = await captureApi(`/capture/project?root=${encodeURIComponent(root)}`);
13534
+ if (!project.brainId || !project.sourceId) {
13535
+ console.error(
13536
+ `brainjar: no Brain is connected to ${root} \u2014 run \`npx brainjar init\` first`
13537
+ );
13538
+ process.exitCode = 1;
13539
+ return;
13540
+ }
13541
+ console.error("brainjar: selecting key files\u2026");
13542
+ const [candidates, graph] = await Promise.all([
13543
+ walkCandidates(root),
13544
+ scanStructure(root)
13545
+ ]);
13546
+ const files = selectDeepScanFiles(candidates, graph);
13547
+ const artifacts = [];
13548
+ for (const rel of files) {
13549
+ try {
13550
+ const abs = path6.join(root, rel);
13551
+ const info = await stat2(abs);
13552
+ if (info.size > RAW_SKIP_BYTES) continue;
13553
+ const content = await readFile6(abs, "utf8");
13554
+ artifacts.push({
13555
+ source: "deepscan",
13556
+ kind: "source_file",
13557
+ externalId: `file:${rel}`,
13558
+ uri: rel,
13559
+ sha: contentHash(content),
13560
+ title: rel,
13561
+ body: content.slice(0, FILE_CAP),
13562
+ author: null,
13563
+ createdAt: info.mtime.toISOString(),
13564
+ updatedAt: info.mtime.toISOString(),
13565
+ payload: { path: rel, bytes: info.size }
13566
+ });
13567
+ } catch {
13568
+ }
13569
+ }
13570
+ let shipped = 0;
13571
+ let queued = 0;
13572
+ let chunk = [];
13573
+ let chunkChars = 0;
13574
+ const flush = async (final) => {
13575
+ if (chunk.length === 0 && !final) return;
13576
+ const r = await captureApi(
13577
+ "/capture/artifacts",
13578
+ {
13579
+ method: "POST",
13580
+ body: {
13581
+ root,
13582
+ sourceId: project.sourceId,
13583
+ artifacts: chunk,
13584
+ // Only the final chunk kicks the server-side synthesis: one scan per run.
13585
+ deepscanFinal: final
13586
+ }
13587
+ }
13588
+ );
13589
+ shipped += chunk.length;
13590
+ queued += r.deepscanQueued ?? 0;
13591
+ chunk = [];
13592
+ chunkChars = 0;
13593
+ };
13594
+ for (const a of artifacts) {
13595
+ if (chunk.length >= CHUNK_FILES || chunkChars + a.body.length > CHUNK_CHARS)
13596
+ await flush(false);
13597
+ chunk.push(a);
13598
+ chunkChars += a.body.length;
13599
+ }
13600
+ await flush(true);
13601
+ console.error(
13602
+ `brainjar: deep scan shipped ${shipped} files (${queued} queued for synthesis).`
13603
+ );
13604
+ console.error(
13605
+ "brainjar: synthesis runs on the server in the background \u2014 the bell in the app reports the outcome. Unchanged files were deduplicated automatically."
13606
+ );
13607
+ }
13608
+ var FILE_CAP, CHUNK_CHARS, CHUNK_FILES, MAX_FILES, RAW_SKIP_BYTES, BUCKET_CAPS;
13609
+ var init_deepscan2 = __esm({
13610
+ "../../packages/cli/src/deepscan.ts"() {
13611
+ "use strict";
13612
+ init_src3();
13613
+ init_api_client();
13614
+ FILE_CAP = 16e3;
13615
+ CHUNK_CHARS = 65e4;
13616
+ CHUNK_FILES = 40;
13617
+ MAX_FILES = 60;
13618
+ RAW_SKIP_BYTES = 2e5;
13619
+ BUCKET_CAPS = [
13620
+ // READMEs first — they outrank everything else the repo says about itself.
13621
+ { cap: 4, test: (p) => /(^|\/)readme[^/]*$/i.test(p) },
13622
+ // Markdown docs.
13623
+ { cap: 6, test: (p) => /^docs\//i.test(p) && p.endsWith(".md") },
13624
+ // Manifests: what the project IS and depends on.
13625
+ {
13626
+ cap: 10,
13627
+ test: (p) => /(^|\/)(package\.json|pyproject\.toml|go\.mod|cargo\.toml|composer\.json|gemfile)$/i.test(
13628
+ p
13629
+ ) && p.split("/").length <= 3
13630
+ },
13631
+ // Schema + deployment: migrations, compose files, example env.
13632
+ {
13633
+ cap: 12,
13634
+ test: (p) => /(^|\/)migrations\//.test(p) && p.endsWith(".sql") || /(^|\/)(docker-compose[^/]*\.ya?ml|\.env\.example|schema\.[^/]+)$/i.test(
13635
+ p
13636
+ )
13637
+ },
13638
+ // Entry points and API surfaces by name.
13639
+ {
13640
+ cap: 12,
13641
+ test: (p) => /(^|\/)(index|main|app|server|cli|routes?|api)\.(ts|tsx|js|py|go|rb)$/.test(
13642
+ p
13643
+ )
13644
+ }
13645
+ ];
13646
+ }
13647
+ });
13648
+
13229
13649
  // ../../packages/cli/src/unhook.ts
13230
13650
  var unhook_exports = {};
13231
13651
  __export(unhook_exports, {
@@ -13275,11 +13695,11 @@ async function dokuMain(argv = []) {
13275
13695
  return;
13276
13696
  }
13277
13697
  let llm;
13278
- const config = loadConfig();
13279
- if (config.cloudAiEnabled) {
13698
+ const key = await resolveAnthropicApiKey(loadEnv().ANTHROPIC_API_KEY);
13699
+ if (key) {
13280
13700
  try {
13281
- llm = anthropicClientFromConfig(
13282
- config,
13701
+ llm = anthropicClient(
13702
+ key,
13283
13703
  await resolveSetting(
13284
13704
  "rationale_model",
13285
13705
  loadEnv().BRAINJAR_RATIONALE_MODEL
@@ -13329,6 +13749,7 @@ Usage:
13329
13749
  brainjar constraints [--hook] Known failed approaches (SessionStart hook entrypoint)
13330
13750
  brainjar failure --hook Near-live failure capture (PostToolUseFailure entrypoint)
13331
13751
  brainjar doctor [--no-sync] Check the capture pipeline end to end
13752
+ brainjar deepscan Read the repo's key files into the Brain (AI synthesis server-side)
13332
13753
  brainjar unhook Remove all BrainJar hooks (data stays)
13333
13754
  brainjar doku [--export] Rebuild the documentation pages (--export dumps them to files)
13334
13755
  brainjar help Show this help
@@ -13361,6 +13782,11 @@ async function main() {
13361
13782
  await doctorMain2(argv);
13362
13783
  break;
13363
13784
  }
13785
+ case "deepscan": {
13786
+ const { deepscanMain: deepscanMain2 } = await Promise.resolve().then(() => (init_deepscan2(), deepscan_exports));
13787
+ await deepscanMain2();
13788
+ break;
13789
+ }
13364
13790
  case "unhook": {
13365
13791
  const { unhookMain: unhookMain2 } = await Promise.resolve().then(() => (init_unhook(), unhook_exports));
13366
13792
  await unhookMain2();
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "brainjar",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "BrainJar CLI: connect a repository to a BrainJar server (capture, hooks, MCP)",
5
- "license": "SEE LICENSE ON https://ponytail.dev",
6
- "author": "ponytailhq <ops@ponytail.dev>",
5
+ "license": "SEE LICENSE ON https://brainjar.dev",
6
+ "author": "Dietrich Gebert",
7
7
  "homepage": "https://brainjar.dev",
8
8
  "type": "module",
9
9
  "bin": {