brainjar 0.3.975 → 0.3.977

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/brainjar.js +818 -155
  2. package/package.json +1 -1
package/brainjar.js CHANGED
@@ -107,7 +107,7 @@ var CLI_VERSION, CACHE, DAY;
107
107
  var init_version_check = __esm({
108
108
  "packages/cli/src/version-check.ts"() {
109
109
  "use strict";
110
- CLI_VERSION = "0.3.975";
110
+ CLI_VERSION = "0.3.977";
111
111
  CACHE = path.join(os.homedir(), ".brainjar", "version-check.json");
112
112
  DAY = 864e5;
113
113
  }
@@ -5895,6 +5895,11 @@ function loadEnv() {
5895
5895
  }
5896
5896
  return { ...cachedFileVars, ...process.env };
5897
5897
  }
5898
+ function estimateCostUsd(model, tokens) {
5899
+ const [base = model, tier] = model.split(":");
5900
+ const p = MODEL_PRICES[base] ?? MODEL_PRICES[MODEL_TIERS.standard];
5901
+ return (tier === "flex" ? 0.5 : 1) * (tokens.input * p.input + tokens.cachedInput * p.cachedInput + tokens.output * p.output) / 1e6;
5902
+ }
5898
5903
  function loadConfig(env = loadEnv()) {
5899
5904
  return new Config(env);
5900
5905
  }
@@ -5907,10 +5912,24 @@ function redactUrlPassword(url) {
5907
5912
  return url;
5908
5913
  }
5909
5914
  }
5910
- var cachedFileVars, DEFAULT_TENANT_ID, SECRETS, CORPORATE_API_KEY_ENV, DEFAULT_DATABASE_URL, MissingSecretError, Config;
5915
+ var cachedFileVars, MODEL_TIERS, MODEL_PRICES, DEFAULT_TENANT_ID, SECRETS, CORPORATE_API_KEY_ENV, DEFAULT_DATABASE_URL, MissingSecretError, Config;
5911
5916
  var init_config = __esm({
5912
5917
  "packages/shared/src/config.ts"() {
5913
5918
  "use strict";
5919
+ MODEL_TIERS = {
5920
+ fast: "claude-haiku-4-5",
5921
+ standard: "claude-sonnet-5",
5922
+ premium: "claude-opus-5"
5923
+ };
5924
+ MODEL_PRICES = {
5925
+ "claude-opus-5": { input: 5, cachedInput: 0.5, output: 25 },
5926
+ // Launch pricing made permanent (official pricing page, checked 2026-08-18).
5927
+ "claude-sonnet-5": { input: 2, cachedInput: 0.2, output: 10 },
5928
+ "claude-haiku-4-5": { input: 1, cachedInput: 0.1, output: 5 },
5929
+ "gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, output: 1.2 },
5930
+ // Released 2026-09-22 (official pricing page, checked 2026-09-25).
5931
+ "gpt-6-luna": { input: 0.1, cachedInput: 0.01, output: 0.5 }
5932
+ };
5914
5933
  DEFAULT_TENANT_ID = "00000000-0000-0000-0000-000000000002";
5915
5934
  SECRETS = {
5916
5935
  anthropicApiKey: {
@@ -6016,13 +6035,6 @@ var init_config = __esm({
6016
6035
  }
6017
6036
  });
6018
6037
 
6019
- // packages/shared/src/ratelimit.ts
6020
- var init_ratelimit = __esm({
6021
- "packages/shared/src/ratelimit.ts"() {
6022
- "use strict";
6023
- }
6024
- });
6025
-
6026
6038
  // packages/shared/src/sanitize.ts
6027
6039
  var init_sanitize = __esm({
6028
6040
  "packages/shared/src/sanitize.ts"() {
@@ -6073,7 +6085,7 @@ function validateFlowDiagram(d) {
6073
6085
  if (n2.evidence.length > 12)
6074
6086
  errs.push(`nodes: ${n2.id} too many evidence entries`);
6075
6087
  for (const ev of n2.evidence)
6076
- if (!short(ev, 200) || /[-]/.test(ev))
6088
+ if (!short(ev, 200) || /[\x00-\x1f]/.test(ev))
6077
6089
  errs.push(`nodes: ${n2.id} evidence entry is not a short string`);
6078
6090
  }
6079
6091
  if (n2.description !== void 0 && !short(n2.description, 600))
@@ -6216,7 +6228,6 @@ var init_src = __esm({
6216
6228
  init_memory();
6217
6229
  init_confidence();
6218
6230
  init_config();
6219
- init_ratelimit();
6220
6231
  init_sanitize();
6221
6232
  init_flow();
6222
6233
  init_capture_events();
@@ -6238,8 +6249,11 @@ function getPool() {
6238
6249
  }
6239
6250
  pool = new esm_default.Pool({
6240
6251
  connectionString: config.databaseUrl,
6252
+ // Per process (BJ-137): many instances times the default would fill Postgres; each role
6253
+ // sets its own in the compose file. PgBouncer in front is the cloud ticket's (BJ-140).
6254
+ max: Number(process.env.DATABASE_POOL_MAX) || 10,
6241
6255
  // Session GUC set in the startup packet — race-free, applies before any query.
6242
- options: `-c app.tenant_id=${config.tenantId}`
6256
+ options: `-c app.tenant_id=${config.tenantId} -c hnsw.iterative_scan=strict_order`
6243
6257
  });
6244
6258
  pool.on("error", () => {
6245
6259
  });
@@ -7599,7 +7613,7 @@ var init_repository = __esm({
7599
7613
  });
7600
7614
 
7601
7615
  // packages/db/src/ratelimit.ts
7602
- var init_ratelimit2 = __esm({
7616
+ var init_ratelimit = __esm({
7603
7617
  "packages/db/src/ratelimit.ts"() {
7604
7618
  "use strict";
7605
7619
  init_pool();
@@ -7629,7 +7643,7 @@ var init_auth = __esm({
7629
7643
  "use strict";
7630
7644
  init_src();
7631
7645
  init_pool();
7632
- init_ratelimit2();
7646
+ init_ratelimit();
7633
7647
  init_auth_event();
7634
7648
  init_email_token();
7635
7649
  DEFAULT_TTL_HOURS = 24 * 90;
@@ -7776,7 +7790,22 @@ var init_settings = __esm({
7776
7790
  // Voice chat (voice handoff, 2026-09-21): an explicit "true" or "false"; absent means on.
7777
7791
  "voice_enabled",
7778
7792
  // The GPT-Live voice (voice picker, 2026-09-22): an id from VOICE_OPTIONS; absent = default.
7779
- "voice_name"
7793
+ "voice_name",
7794
+ // AI cost cap (BJ-139), set on the Operator page; env overrides, defaults in ingest/allowance.
7795
+ "ai_contingent",
7796
+ // "on" | "off": monthly contingent per workspace (default on in SaaS)
7797
+ "ai_free_monthly_usd",
7798
+ // Free workspace, USD per month
7799
+ "ai_team_seat_monthly_usd",
7800
+ // Team, USD per seat and month (applies once licenses exist)
7801
+ "ai_operator_exempt",
7802
+ // "true" | "false": operator accounts are never capped
7803
+ "ai_daily_budget_usd",
7804
+ // the instance's emergency brake on the company key; empty = off
7805
+ "ai_budget_released_day",
7806
+ // YYYY-MM-DD the operator lifted today's brake
7807
+ "ai_budget_alerted_day"
7808
+ // YYYY-MM-DD the operator was told the brake tripped
7780
7809
  ];
7781
7810
  PROVIDER_KEY_SETTINGS = {
7782
7811
  anthropic: "anthropic_api_key",
@@ -7810,10 +7839,102 @@ var init_tickets = __esm({
7810
7839
  });
7811
7840
 
7812
7841
  // packages/db/src/jobs.ts
7842
+ var CAPTURE_JOB_KINDS, USER_JOB_KINDS;
7813
7843
  var init_jobs = __esm({
7814
7844
  "packages/db/src/jobs.ts"() {
7815
7845
  "use strict";
7816
7846
  init_pool();
7847
+ CAPTURE_JOB_KINDS = [
7848
+ "capture-extract",
7849
+ "capture-session",
7850
+ "capture-transcript",
7851
+ "capture-structure"
7852
+ ];
7853
+ USER_JOB_KINDS = [
7854
+ ...CAPTURE_JOB_KINDS,
7855
+ "deepscan",
7856
+ "verify",
7857
+ "summary-report",
7858
+ "resynthesize"
7859
+ ];
7860
+ }
7861
+ });
7862
+
7863
+ // packages/db/src/spend.ts
7864
+ function dayKey(at = /* @__PURE__ */ new Date()) {
7865
+ return `day:${at.toISOString().slice(0, 10)}`;
7866
+ }
7867
+ async function addSpend(brainId, usd, at = /* @__PURE__ */ new Date(), pool2 = getPool()) {
7868
+ if (!(usd > 0)) return;
7869
+ await pool2.query(
7870
+ `INSERT INTO llm_spend (key, usd)
7871
+ SELECT k, $2 FROM (
7872
+ SELECT $3::text AS k
7873
+ UNION ALL
7874
+ SELECT 'ws:' || b.owner_id || ':' || $4 FROM brain b
7875
+ WHERE b.id = $1 AND b.owner_id IS NOT NULL
7876
+ ) keys
7877
+ ON CONFLICT (key) DO UPDATE SET usd = llm_spend.usd + EXCLUDED.usd, updated_at = now()`,
7878
+ [brainId, usd, dayKey(at), at.toISOString().slice(0, 7)]
7879
+ );
7880
+ }
7881
+ var init_spend = __esm({
7882
+ "packages/db/src/spend.ts"() {
7883
+ "use strict";
7884
+ init_pool();
7885
+ }
7886
+ });
7887
+
7888
+ // packages/db/src/provider-rate.ts
7889
+ async function saveRate(keyId2, model, s, pool2 = getPool()) {
7890
+ await pool2.query(
7891
+ `INSERT INTO llm_rate (key_id, model, requests_limit, requests_remaining, requests_reset_at,
7892
+ tokens_limit, tokens_remaining, tokens_reset_at, observed_at)
7893
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
7894
+ ON CONFLICT (key_id, model) DO UPDATE SET
7895
+ requests_limit = EXCLUDED.requests_limit,
7896
+ requests_remaining = EXCLUDED.requests_remaining,
7897
+ requests_reset_at = EXCLUDED.requests_reset_at,
7898
+ tokens_limit = EXCLUDED.tokens_limit,
7899
+ tokens_remaining = EXCLUDED.tokens_remaining,
7900
+ tokens_reset_at = EXCLUDED.tokens_reset_at,
7901
+ observed_at = EXCLUDED.observed_at
7902
+ WHERE llm_rate.observed_at <= EXCLUDED.observed_at`,
7903
+ [
7904
+ keyId2,
7905
+ model,
7906
+ s.requestsLimit,
7907
+ s.requestsRemaining,
7908
+ s.requestsResetAt,
7909
+ s.tokensLimit,
7910
+ s.tokensRemaining,
7911
+ s.tokensResetAt,
7912
+ s.observedAt
7913
+ ]
7914
+ );
7915
+ }
7916
+ async function loadRate(keyId2, model, pool2 = getPool()) {
7917
+ const r = await pool2.query(
7918
+ "SELECT * FROM llm_rate WHERE key_id = $1 AND model = $2",
7919
+ [keyId2, model]
7920
+ );
7921
+ const x = r.rows[0];
7922
+ if (!x) return null;
7923
+ const n2 = (v) => v === null ? null : Number(v);
7924
+ return {
7925
+ requestsLimit: n2(x.requests_limit),
7926
+ requestsRemaining: n2(x.requests_remaining),
7927
+ requestsResetAt: x.requests_reset_at,
7928
+ tokensLimit: n2(x.tokens_limit),
7929
+ tokensRemaining: n2(x.tokens_remaining),
7930
+ tokensResetAt: x.tokens_reset_at,
7931
+ observedAt: x.observed_at
7932
+ };
7933
+ }
7934
+ var init_provider_rate = __esm({
7935
+ "packages/db/src/provider-rate.ts"() {
7936
+ "use strict";
7937
+ init_pool();
7817
7938
  }
7818
7939
  });
7819
7940
 
@@ -7841,11 +7962,21 @@ async function recordLlmUsage(u) {
7841
7962
  Math.max(0, Math.round(u.outputTokens))
7842
7963
  ]
7843
7964
  );
7965
+ await addSpend(
7966
+ u.brainId ?? null,
7967
+ estimateCostUsd(u.model, {
7968
+ input: u.inputTokens,
7969
+ cachedInput: u.cachedInputTokens,
7970
+ output: u.outputTokens
7971
+ })
7972
+ );
7844
7973
  }
7845
7974
  var init_usage = __esm({
7846
7975
  "packages/db/src/usage.ts"() {
7847
7976
  "use strict";
7977
+ init_src();
7848
7978
  init_pool();
7979
+ init_spend();
7849
7980
  }
7850
7981
  });
7851
7982
 
@@ -7859,7 +7990,7 @@ var init_src2 = __esm({
7859
7990
  init_auth();
7860
7991
  init_api_token();
7861
7992
  init_oauth();
7862
- init_ratelimit2();
7993
+ init_ratelimit();
7863
7994
  init_auth_event();
7864
7995
  init_email_token();
7865
7996
  init_totp2();
@@ -7872,6 +8003,8 @@ var init_src2 = __esm({
7872
8003
  init_conversations();
7873
8004
  init_tickets();
7874
8005
  init_jobs();
8006
+ init_spend();
8007
+ init_provider_rate();
7875
8008
  init_ruleset();
7876
8009
  init_usage();
7877
8010
  init_audit_origin();
@@ -12740,11 +12873,118 @@ var init_sdk = __esm({
12740
12873
  }
12741
12874
  });
12742
12875
 
12876
+ // packages/ingest/src/provider-rate.ts
12877
+ import { createHash as createHash2 } from "node:crypto";
12878
+ function parseDuration(s) {
12879
+ if (!s) return null;
12880
+ let ms = 0;
12881
+ let hit = false;
12882
+ for (const [, n2, unit] of s.matchAll(/(\d+(?:\.\d+)?)(ms|h|m|s)/g)) {
12883
+ hit = true;
12884
+ ms += Number(n2) * UNIT_MS[unit];
12885
+ }
12886
+ return hit ? ms : null;
12887
+ }
12888
+ function parseRateHeaders(h, now = /* @__PURE__ */ new Date()) {
12889
+ const num = (a, b) => {
12890
+ const v = h.get(a) ?? h.get(b);
12891
+ const n2 = v === null ? NaN : Number(v);
12892
+ return Number.isFinite(n2) ? n2 : null;
12893
+ };
12894
+ const at = (openai, anthropic) => {
12895
+ const d = parseDuration(h.get(openai));
12896
+ if (d !== null) return new Date(now.getTime() + d);
12897
+ const t = Date.parse(h.get(anthropic) ?? "");
12898
+ return Number.isFinite(t) ? new Date(t) : null;
12899
+ };
12900
+ const s = {
12901
+ requestsLimit: num(
12902
+ "x-ratelimit-limit-requests",
12903
+ "anthropic-ratelimit-requests-limit"
12904
+ ),
12905
+ requestsRemaining: num(
12906
+ "x-ratelimit-remaining-requests",
12907
+ "anthropic-ratelimit-requests-remaining"
12908
+ ),
12909
+ requestsResetAt: at(
12910
+ "x-ratelimit-reset-requests",
12911
+ "anthropic-ratelimit-requests-reset"
12912
+ ),
12913
+ tokensLimit: num(
12914
+ "x-ratelimit-limit-tokens",
12915
+ "anthropic-ratelimit-tokens-limit"
12916
+ ),
12917
+ tokensRemaining: num(
12918
+ "x-ratelimit-remaining-tokens",
12919
+ "anthropic-ratelimit-tokens-remaining"
12920
+ ),
12921
+ tokensResetAt: at(
12922
+ "x-ratelimit-reset-tokens",
12923
+ "anthropic-ratelimit-tokens-reset"
12924
+ ),
12925
+ observedAt: now
12926
+ };
12927
+ return s.requestsLimit === null && s.tokensLimit === null ? null : s;
12928
+ }
12929
+ function roomWaitMs(s, now = /* @__PURE__ */ new Date(), reserve = RESERVE) {
12930
+ const wait = (limit, remaining, resetAt) => {
12931
+ if (!limit || remaining === null || !resetAt) return 0;
12932
+ const need = reserve * limit;
12933
+ if (remaining >= need || resetAt <= now) return 0;
12934
+ const span = resetAt.getTime() - s.observedAt.getTime();
12935
+ const okAt = s.observedAt.getTime() + span * (need - remaining) / (limit - remaining);
12936
+ return Math.max(0, Math.ceil(okAt - now.getTime()));
12937
+ };
12938
+ return Math.max(
12939
+ wait(s.requestsLimit, s.requestsRemaining, s.requestsResetAt),
12940
+ wait(s.tokensLimit, s.tokensRemaining, s.tokensResetAt)
12941
+ );
12942
+ }
12943
+ async function awaitRoom(id, model) {
12944
+ for (; ; ) {
12945
+ const s = await loadRate(id, model).catch(() => null);
12946
+ const ms = s ? roomWaitMs(s) : 0;
12947
+ if (ms <= 0) return;
12948
+ await sleep2(Math.min(ms, MAX_NAP_MS) + Math.random() * 250);
12949
+ }
12950
+ }
12951
+ function noteRate(id, model, headers) {
12952
+ const s = headers ? parseRateHeaders(headers) : null;
12953
+ if (s) void saveRate(id, model, s).catch(() => {
12954
+ });
12955
+ }
12956
+ function rateFetch(apiKey, model, automatic) {
12957
+ const id = keyId(apiKey);
12958
+ return async (input, init2) => {
12959
+ if (automatic) await awaitRoom(id, model);
12960
+ const res = await fetch(input, init2);
12961
+ noteRate(id, model, res.headers);
12962
+ return res;
12963
+ };
12964
+ }
12965
+ var RESERVE, MAX_NAP_MS, sleep2, isAutomatic, keyId, UNIT_MS;
12966
+ var init_provider_rate2 = __esm({
12967
+ "packages/ingest/src/provider-rate.ts"() {
12968
+ "use strict";
12969
+ init_src2();
12970
+ RESERVE = 0.2;
12971
+ MAX_NAP_MS = 6e4;
12972
+ sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12973
+ isAutomatic = (meta) => !meta?.actor && !meta?.waiting;
12974
+ keyId = (apiKey) => createHash2("sha256").update(apiKey).digest("hex").slice(0, 16);
12975
+ UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
12976
+ }
12977
+ });
12978
+
12743
12979
  // packages/ingest/src/llm.ts
12744
12980
  function rationaleModelId(env = process.env) {
12745
12981
  return env.BRAINJAR_RATIONALE_MODEL?.trim() || "claude-opus-5";
12746
12982
  }
12747
12983
  function logUsage(meta, model, usage) {
12984
+ if (!meta?.brainId)
12985
+ console.warn(
12986
+ `brainjar: AI call without a Brain (workload ${meta?.workload ?? "other"}); it counts only toward the daily budget`
12987
+ );
12748
12988
  void recordLlmUsage({
12749
12989
  brainId: meta?.brainId ?? null,
12750
12990
  workload: meta?.workload ?? "other",
@@ -12770,6 +13010,7 @@ var init_llm = __esm({
12770
13010
  "use strict";
12771
13011
  init_sdk();
12772
13012
  init_src2();
13013
+ init_provider_rate2();
12773
13014
  cachedSystem = (system) => [
12774
13015
  {
12775
13016
  type: "text",
@@ -12782,9 +13023,12 @@ var init_llm = __esm({
12782
13023
  model;
12783
13024
  meta;
12784
13025
  constructor(opts) {
12785
- this.client = new Anthropic({ apiKey: opts.apiKey });
12786
13026
  this.model = opts.model ?? rationaleModelId();
12787
13027
  this.meta = opts.meta;
13028
+ this.client = new Anthropic({
13029
+ apiKey: opts.apiKey,
13030
+ fetch: rateFetch(opts.apiKey, this.model, isAutomatic(opts.meta))
13031
+ });
12788
13032
  }
12789
13033
  async complete({
12790
13034
  system,
@@ -12831,7 +13075,18 @@ var init_llm = __esm({
12831
13075
  var init_openai = __esm({
12832
13076
  "packages/ingest/src/openai.ts"() {
12833
13077
  "use strict";
13078
+ init_src();
12834
13079
  init_llm();
13080
+ init_provider_rate2();
13081
+ }
13082
+ });
13083
+
13084
+ // packages/ingest/src/allowance.ts
13085
+ var init_allowance = __esm({
13086
+ "packages/ingest/src/allowance.ts"() {
13087
+ "use strict";
13088
+ init_src2();
13089
+ init_src();
12835
13090
  }
12836
13091
  });
12837
13092
 
@@ -12841,6 +13096,7 @@ var init_routing = __esm({
12841
13096
  "use strict";
12842
13097
  init_src2();
12843
13098
  init_src();
13099
+ init_allowance();
12844
13100
  init_llm();
12845
13101
  init_openai();
12846
13102
  }
@@ -12928,42 +13184,280 @@ function resolveSpecifier(fromFile, spec, known) {
12928
13184
  for (const probe of [
12929
13185
  clean,
12930
13186
  c,
12931
- ...SOURCE_EXTS.map((e) => clean + e),
12932
- ...SOURCE_EXTS.map((e) => `${clean}/index${e}`)
13187
+ ...JS_EXTS.map((e) => clean + e),
13188
+ ...JS_EXTS.map((e) => `${clean}/index${e}`)
12933
13189
  ]) {
12934
13190
  if (known.has(probe)) return probe;
12935
13191
  }
12936
13192
  }
12937
13193
  return null;
12938
13194
  }
13195
+ function resolveBySuffix(segments, exts, fromFile, idx, inits = []) {
13196
+ if (segments.length === 0) return null;
13197
+ const want = segments.join("/");
13198
+ const last = segments[segments.length - 1];
13199
+ const hits = [
13200
+ ...(idx.byName.get(last) ?? []).filter(
13201
+ (f) => exts.includes(ext(f)) && (stem(f) === want || stem(f).endsWith(`/${want}`))
13202
+ ),
13203
+ ...inits.flatMap(
13204
+ (init2) => (idx.byName.get(stem(init2)) ?? []).filter(
13205
+ (f) => f.endsWith(`/${want}/${init2}`) || f === `${want}/${init2}`
13206
+ )
13207
+ )
13208
+ ];
13209
+ if (hits.length === 0) return null;
13210
+ return hits.sort(
13211
+ (a, b) => nearness(b, fromFile) - nearness(a, fromFile) || a.length - b.length
13212
+ )[0];
13213
+ }
13214
+ function pythonTargets(file, src, idx) {
13215
+ const out = [];
13216
+ const mod = (base, segs) => resolveBySuffix([...base, ...segs], [".py"], file, idx, ["__init__.py"]);
13217
+ for (const m of src.matchAll(PY_FROM)) {
13218
+ const dots = m[1].length;
13219
+ const segs = m[2] ? m[2].split(".") : [];
13220
+ const names = m[3].split(",").map((n2) => n2.trim().split(/\s+/)[0]);
13221
+ if (dots > 0) {
13222
+ let dir = path4.posix.dirname(file);
13223
+ for (let i = 1; i < dots; i++) dir = path4.posix.dirname(dir);
13224
+ const base = dir === "." ? [] : dir.split("/");
13225
+ const exact = (s) => {
13226
+ const p = [...base, ...s].join("/");
13227
+ return idx.known.has(`${p}.py`) ? `${p}.py` : idx.known.has(`${p}/__init__.py`) ? `${p}/__init__.py` : null;
13228
+ };
13229
+ const target = segs.length ? exact(segs) : null;
13230
+ if (target) out.push(target);
13231
+ for (const n2 of names) {
13232
+ const sub = exact([...segs, n2]);
13233
+ if (sub) out.push(sub);
13234
+ }
13235
+ } else if (segs.length) {
13236
+ const sub = names.map((n2) => mod([], [...segs, n2])).filter(Boolean);
13237
+ const target = resolveLongest(segs, (s) => mod([], s));
13238
+ out.push(...sub, ...target ? [target] : []);
13239
+ }
13240
+ }
13241
+ for (const m of src.matchAll(PY_IMPORT))
13242
+ for (const spec of m[1].split(",")) {
13243
+ const target = resolveLongest(spec.trim().split("."), (s) => mod([], s));
13244
+ if (target) out.push(target);
13245
+ }
13246
+ return out;
13247
+ }
13248
+ function rustTargets(file, src, idx) {
13249
+ const out = [];
13250
+ const dir = path4.posix.dirname(file);
13251
+ const base = path4.posix.basename(file, ".rs");
13252
+ const own = ["lib", "main", "mod"].includes(base) ? dir : `${dir}/${base}`;
13253
+ const crateSrc = [...idx.crates.values()].filter((s) => file.startsWith(`${s}/`)).sort((a, b) => b.length - a.length)[0];
13254
+ const inDir = (d, segs) => {
13255
+ const p = [d, ...segs].join("/");
13256
+ return idx.known.has(`${p}.rs`) ? `${p}.rs` : idx.known.has(`${p}/mod.rs`) ? `${p}/mod.rs` : null;
13257
+ };
13258
+ for (const m of src.matchAll(RS_MOD)) {
13259
+ const hit = inDir(own, [m[1]]);
13260
+ if (hit) out.push(hit);
13261
+ }
13262
+ for (const m of src.matchAll(RS_USE))
13263
+ for (const use of expandUse(m[1])) {
13264
+ const [head, ...rest] = use.replace(/^::/, "").split("::");
13265
+ let root;
13266
+ if (head === "crate") root = crateSrc;
13267
+ else if (head === "self") root = own;
13268
+ else if (head === "super")
13269
+ root = ["lib", "main", "mod"].includes(base) ? path4.posix.dirname(dir) : dir;
13270
+ else if (head && idx.crates.has(head)) {
13271
+ root = idx.crates.get(head);
13272
+ if (rest.length === 0) {
13273
+ const lib = `${root}/lib.rs`;
13274
+ if (idx.known.has(lib)) out.push(lib);
13275
+ continue;
13276
+ }
13277
+ }
13278
+ if (!root) continue;
13279
+ const hit = resolveLongest(rest, (s) => inDir(root, s));
13280
+ if (hit) out.push(hit);
13281
+ else if (head !== "self" && head !== "super") {
13282
+ const lib = `${root}/lib.rs`;
13283
+ if (idx.known.has(lib)) out.push(lib);
13284
+ }
13285
+ }
13286
+ return out;
13287
+ }
13288
+ function goTargets(file, src, idx) {
13289
+ const specs = [
13290
+ ...[...src.matchAll(GO_BLOCK)].flatMap(
13291
+ (m) => [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1])
13292
+ ),
13293
+ ...[...src.matchAll(GO_ONE)].map((m) => m[1])
13294
+ ];
13295
+ const out = [];
13296
+ for (const spec of specs)
13297
+ for (const [mod, dir] of idx.goModules) {
13298
+ if (spec !== mod && !spec.startsWith(`${mod}/`)) continue;
13299
+ const pkgDir = [dir === "." ? "" : dir, spec.slice(mod.length + 1)].filter(Boolean).join("/");
13300
+ const rep = (idx.byDir.get(pkgDir) ?? []).find(
13301
+ (f) => f.endsWith(".go") && !f.endsWith("_test.go")
13302
+ );
13303
+ if (rep) out.push(rep);
13304
+ }
13305
+ return out;
13306
+ }
13307
+ function swiftTargets(src, idx) {
13308
+ const out = [];
13309
+ for (const m of src.matchAll(
13310
+ /^[ \t]*(?:@\w+[ \t]+)*import[ \t]+(?:\w+[ \t]+)?(\w+)/gm
13311
+ )) {
13312
+ for (const [dir, files] of idx.byDir) {
13313
+ if (!(dir === `Sources/${m[1]}` || dir.endsWith(`/Sources/${m[1]}`) || dir.includes(`/Sources/${m[1]}/`)))
13314
+ continue;
13315
+ const rep = files.find((f) => f.endsWith(".swift") || f.endsWith(".h"));
13316
+ if (rep) {
13317
+ out.push(rep);
13318
+ break;
13319
+ }
13320
+ }
13321
+ }
13322
+ return out;
13323
+ }
13324
+ function jvmTargets(file, src, idx) {
13325
+ const out = [];
13326
+ for (const m of src.matchAll(
13327
+ /^[ \t]*import[ \t]+(?:static[ \t]+)?([\w.]+)/gm
13328
+ )) {
13329
+ const hit = resolveLongest(
13330
+ m[1].split("."),
13331
+ (s) => resolveBySuffix(s, [".java", ".kt"], file, idx)
13332
+ );
13333
+ if (hit) out.push(hit);
13334
+ }
13335
+ return out;
13336
+ }
13337
+ function cTargets(file, src, idx) {
13338
+ const out = [];
13339
+ for (const m of src.matchAll(
13340
+ /^[ \t]*#[ \t]*(?:include|import)[ \t]*"([^"]+)"/gm
13341
+ )) {
13342
+ const rel = path4.posix.normalize(
13343
+ path4.posix.join(path4.posix.dirname(file), m[1])
13344
+ );
13345
+ if (idx.known.has(rel)) out.push(rel);
13346
+ else {
13347
+ const hit = resolveBySuffix(
13348
+ stem(m[1]).split("/"),
13349
+ [ext(m[1])],
13350
+ file,
13351
+ idx
13352
+ );
13353
+ if (hit) out.push(hit);
13354
+ }
13355
+ }
13356
+ return out;
13357
+ }
12939
13358
  async function scanStructure(root) {
12940
13359
  const entries = await readdir(root, { recursive: true, withFileTypes: true });
12941
13360
  const files = [];
13361
+ const cargo = [];
13362
+ const gomod = [];
12942
13363
  for (const e of entries) {
12943
13364
  if (!e.isFile()) continue;
12944
13365
  const parent = e.parentPath ?? e.path ?? root;
12945
13366
  const rel = toPosix(path4.relative(root, path4.join(parent, e.name)));
12946
13367
  if (rel.split("/").some((seg) => SKIP_DIRS.has(seg))) continue;
12947
- if (SOURCE_EXTS.some((ext) => rel.endsWith(ext))) files.push(rel);
13368
+ if (SOURCE_EXTS.some((x) => rel.endsWith(x))) files.push(rel);
13369
+ else if (e.name === "Cargo.toml") cargo.push(rel);
13370
+ else if (e.name === "go.mod") gomod.push(rel);
13371
+ }
13372
+ const read = (f) => readFile(path4.join(root, f), "utf8");
13373
+ const idx = {
13374
+ known: new Set(files),
13375
+ byName: /* @__PURE__ */ new Map(),
13376
+ byDir: /* @__PURE__ */ new Map(),
13377
+ crates: /* @__PURE__ */ new Map(),
13378
+ goModules: /* @__PURE__ */ new Map()
13379
+ };
13380
+ for (const f of [...files].sort()) {
13381
+ const name = path4.posix.basename(stem(f));
13382
+ idx.byName.set(name, [...idx.byName.get(name) ?? [], f]);
13383
+ const d = path4.posix.dirname(f);
13384
+ idx.byDir.set(d, [...idx.byDir.get(d) ?? [], f]);
13385
+ }
13386
+ for (const c of cargo) {
13387
+ const pkg = (await read(c)).match(
13388
+ /\[package\][^[]*?\bname\s*=\s*"([^"]+)"/
13389
+ );
13390
+ const dir = path4.posix.dirname(c);
13391
+ if (pkg)
13392
+ idx.crates.set(
13393
+ pkg[1].replace(/-/g, "_"),
13394
+ `${dir === "." ? "" : `${dir}/`}src`.replace(/^\//, "")
13395
+ );
12948
13396
  }
12949
- const known = new Set(files);
13397
+ for (const g of gomod) {
13398
+ const mod = (await read(g)).match(/^module\s+(\S+)/m);
13399
+ if (mod) idx.goModules.set(mod[1], path4.posix.dirname(g));
13400
+ }
13401
+ const exported = /* @__PURE__ */ new Map();
13402
+ for (const f of files.filter((x) => x.endsWith(".rs")))
13403
+ for (const m of (await read(f)).matchAll(RS_EXPORT))
13404
+ if (!exported.has(m[1])) exported.set(m[1], f);
13405
+ const names = [...exported.keys()].slice(0, 2e3);
13406
+ const callRe = names.length ? new RegExp(
13407
+ `\\b(${names.map((n2) => n2.replace(/[^\w]/g, "")).join("|")})\\s*\\(`,
13408
+ "g"
13409
+ ) : null;
13410
+ const seen = /* @__PURE__ */ new Set();
12950
13411
  const edges = [];
13412
+ const add = (from, to) => {
13413
+ if (!to || to === from || !idx.known.has(to)) return;
13414
+ const k = JSON.stringify([from, to]);
13415
+ if (seen.has(k)) return;
13416
+ seen.add(k);
13417
+ edges.push({ from, to });
13418
+ };
12951
13419
  for (const file of files) {
12952
- const source = await readFile(path4.join(root, file), "utf8");
12953
- for (const spec of extractImports(source)) {
12954
- const to = resolveSpecifier(file, spec, known);
12955
- if (to && to !== file) edges.push({ from: file, to });
12956
- }
13420
+ const src = await read(file);
13421
+ const x = ext(file);
13422
+ if (JS_EXTS.includes(x))
13423
+ for (const spec of extractImports(src))
13424
+ add(file, resolveSpecifier(file, spec, idx.known));
13425
+ else if (x === ".py")
13426
+ for (const t of pythonTargets(file, src, idx)) add(file, t);
13427
+ else if (x === ".rs")
13428
+ for (const t of rustTargets(file, src, idx)) add(file, t);
13429
+ else if (x === ".go")
13430
+ for (const t of goTargets(file, src, idx)) add(file, t);
13431
+ else if (x === ".swift")
13432
+ for (const t of swiftTargets(src, idx)) add(file, t);
13433
+ else if (x === ".java" || x === ".kt")
13434
+ for (const t of jvmTargets(file, src, idx)) add(file, t);
13435
+ else if (C_EXTS.includes(x))
13436
+ for (const t of cTargets(file, src, idx)) add(file, t);
13437
+ if (callRe && x !== ".rs")
13438
+ for (const m of src.matchAll(callRe))
13439
+ add(file, exported.get(m[1]) ?? null);
12957
13440
  }
12958
13441
  return { modules: files.sort(), edges };
12959
13442
  }
12960
- var SOURCE_EXTS, SKIP_DIRS, IMPORT_RE, toPosix;
13443
+ var JS_EXTS, C_EXTS, SOURCE_EXTS, SKIP_DIRS, IMPORT_RE, toPosix, ext, stem, nearness, resolveLongest, PY_FROM, PY_IMPORT, RS_MOD, RS_USE, expandUse, GO_BLOCK, GO_ONE, RS_EXPORT;
12961
13444
  var init_structure = __esm({
12962
13445
  "packages/ingest/src/structure.ts"() {
12963
13446
  "use strict";
12964
13447
  init_src2();
12965
13448
  init_entities();
12966
- SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
13449
+ JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
13450
+ C_EXTS = [".c", ".h", ".cc", ".cpp", ".hpp", ".m", ".mm"];
13451
+ SOURCE_EXTS = [
13452
+ ...JS_EXTS,
13453
+ ".py",
13454
+ ".rs",
13455
+ ".go",
13456
+ ".swift",
13457
+ ".java",
13458
+ ".kt",
13459
+ ...C_EXTS
13460
+ ];
12967
13461
  SKIP_DIRS = /* @__PURE__ */ new Set([
12968
13462
  "node_modules",
12969
13463
  ".git",
@@ -12973,10 +13467,51 @@ var init_structure = __esm({
12973
13467
  ".next",
12974
13468
  "dist",
12975
13469
  "build",
12976
- "coverage"
13470
+ "coverage",
13471
+ // Build output and dependency caches of the other languages.
13472
+ "target",
13473
+ ".build",
13474
+ "__pycache__",
13475
+ ".venv",
13476
+ "venv",
13477
+ "vendor",
13478
+ "Pods",
13479
+ "DerivedData",
13480
+ ".gradle"
12977
13481
  ]);
12978
13482
  IMPORT_RE = /(?:import|export)\s+[^'"]*?from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
12979
13483
  toPosix = (p) => p.split(path4.sep).join("/");
13484
+ ext = (f) => path4.posix.extname(f);
13485
+ stem = (f) => f.slice(0, f.length - ext(f).length);
13486
+ nearness = (a, b) => {
13487
+ const x = a.split("/");
13488
+ const y = b.split("/");
13489
+ let i = 0;
13490
+ while (i < x.length && i < y.length && x[i] === y[i]) i++;
13491
+ return i;
13492
+ };
13493
+ resolveLongest = (segments, resolve) => {
13494
+ for (let n2 = segments.length; n2 > 0; n2--) {
13495
+ const hit = resolve(segments.slice(0, n2));
13496
+ if (hit) return hit;
13497
+ }
13498
+ return null;
13499
+ };
13500
+ PY_FROM = /^[ \t]*from[ \t]+(\.*)([\w.]*)[ \t]+import[ \t]+\(?([^\n#)]+)/gm;
13501
+ PY_IMPORT = /^[ \t]*import[ \t]+([\w.]+(?:[ \t]*,[ \t]*[\w.]+)*)/gm;
13502
+ RS_MOD = /^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?mod[ \t]+(\w+)[ \t]*;/gm;
13503
+ RS_USE = /\buse[ \t]+([^;]+);/g;
13504
+ expandUse = (use) => {
13505
+ const clean = use.replace(/\s+/g, "");
13506
+ const brace = clean.match(/^(.*?)::\{(.*)\}$/);
13507
+ if (!brace) return [clean.replace(/::\*$/, "")];
13508
+ return brace[2].split(",").filter(Boolean).map(
13509
+ (part) => part === "self" ? brace[1] : `${brace[1]}::${part.replace(/::\*$/, "")}`
13510
+ );
13511
+ };
13512
+ GO_BLOCK = /\bimport[ \t]*\(([\s\S]*?)\)/g;
13513
+ GO_ONE = /\bimport[ \t]+(?:[\w.]+[ \t]+)?"([^"]+)"/g;
13514
+ RS_EXPORT = /extern[ \t]+"C"[ \t]+fn[ \t]+([A-Za-z_]\w{2,})/g;
12980
13515
  }
12981
13516
  });
12982
13517
 
@@ -14394,6 +14929,8 @@ var init_summaries = __esm({
14394
14929
  "use strict";
14395
14930
  init_src2();
14396
14931
  init_src();
14932
+ init_routing();
14933
+ init_allowance();
14397
14934
  REPORT_SYSTEM = [
14398
14935
  "You write the period report of a software project for the people who work on it: what changed, why it matters, what was decided, what is still open.",
14399
14936
  "Input: the captured records inside the period, numbered, oldest first, each with its date, time, type and title. Record types: feature (behavior that was built or changed), decision (a recorded direction, NOT a delivery), failure (an approach that did not work), event (a commit or another recorded event), note (a note or follow-up).",
@@ -15094,7 +15631,7 @@ var init_doku_model = __esm({
15094
15631
  });
15095
15632
 
15096
15633
  // packages/ingest/src/doku.ts
15097
- import { createHash as createHash2 } from "node:crypto";
15634
+ import { createHash as createHash3 } from "node:crypto";
15098
15635
  import { mkdir as mkdir10, readFile as readFile10, rm as rm6, stat, writeFile as writeFile10 } from "node:fs/promises";
15099
15636
  import path15 from "node:path";
15100
15637
  function bodyLocations(o) {
@@ -15200,6 +15737,45 @@ function aiState(m) {
15200
15737
  aiAttempts: typeof m?.aiAttempts === "number" ? m.aiAttempts : 0
15201
15738
  };
15202
15739
  }
15740
+ function aiFacts(kind, m) {
15741
+ if (!m) return null;
15742
+ if (kind === "feature") {
15743
+ const f = m;
15744
+ return JSON.stringify([
15745
+ f.title,
15746
+ f.what,
15747
+ f.behavior ?? null,
15748
+ f.config ?? [],
15749
+ f.locations ?? [],
15750
+ f.why ?? null
15751
+ ]);
15752
+ }
15753
+ if (kind === "entry") {
15754
+ const e = m;
15755
+ return JSON.stringify([
15756
+ e.brainName,
15757
+ e.description ?? null,
15758
+ (e.architecture?.groups ?? []).map((g) => g.name),
15759
+ // Tuples, never objects: a stored model comes back from jsonb with its keys reordered.
15760
+ (e.architecture?.edges ?? []).map((x) => [x.from, x.to]),
15761
+ (e.features ?? []).map((f) => [f.slug, f.title, f.area, f.what])
15762
+ ]);
15763
+ }
15764
+ return null;
15765
+ }
15766
+ function withStoredAi(model, stored) {
15767
+ const s = stored;
15768
+ const fresh = model;
15769
+ return {
15770
+ ...model,
15771
+ ...s.presentation ? { presentation: s.presentation } : {},
15772
+ body: s.body ?? null,
15773
+ docTitle: s.docTitle ?? null,
15774
+ category: s.category ?? null,
15775
+ ...!fresh.flow && s.flow ? { flow: s.flow } : {},
15776
+ ...!fresh.flowDiagram && s.flowDiagram ? { flowDiagram: s.flowDiagram } : {}
15777
+ };
15778
+ }
15203
15779
  function aiComplete(kind, m) {
15204
15780
  if (!m) return false;
15205
15781
  const text = (v) => typeof v === "string" && v.trim().length > 0;
@@ -15215,6 +15791,28 @@ function aiComplete(kind, m) {
15215
15791
  return true;
15216
15792
  }
15217
15793
  }
15794
+ function pagePrompt(input) {
15795
+ const { categories, flowDiagramTitle, hasFlow, sharedLines = [] } = input;
15796
+ return [
15797
+ "Categories (pick exactly one key):",
15798
+ ...categories.length ? categories.map((c) => `- ${c.key}: ${c.name} (${c.description})`) : ["- general: General (everything)"],
15799
+ "",
15800
+ "Available diagrams (the only allowed flow block sources):",
15801
+ ...flowDiagramTitle ? [
15802
+ `- flowDiagram: "${flowDiagramTitle}" (the feature's runtime flow, lanes and stations)`
15803
+ ] : [],
15804
+ ...hasFlow && !flowDiagramTitle ? ["- flow: the feature's runtime steps as a simple chain"] : [],
15805
+ ...!flowDiagramTitle && !hasFlow ? ["- (none)"] : [],
15806
+ "",
15807
+ ...sharedLines.length ? [
15808
+ "Documented elsewhere for the same endpoint (proven facts; reuse them, link the page):",
15809
+ ...sharedLines,
15810
+ ""
15811
+ ] : [],
15812
+ "Fact sheet:",
15813
+ input.factSheet
15814
+ ].join("\n");
15815
+ }
15218
15816
  function parseDocPage(raw, categoryKeys, ctx) {
15219
15817
  const bad = (error2) => ({ page: null, error: error2, dropped: [] });
15220
15818
  const match = raw.match(/\{[\s\S]*\}/);
@@ -15358,16 +15956,32 @@ function parseArchNotes(raw, groups) {
15358
15956
  };
15359
15957
  }
15360
15958
  function archFlowsInput(model) {
15959
+ const modules = (model.raw?.modules ?? []).filter(
15960
+ (m) => !/\.test\.[a-z]+$/.test(m)
15961
+ );
15962
+ const cited = new Set(model.features.flatMap((f) => f.locations));
15361
15963
  const files = /* @__PURE__ */ new Map();
15362
- for (const m of model.raw?.modules ?? []) {
15363
- if (/\.test\.[a-z]+$/.test(m)) continue;
15964
+ for (const m of [
15965
+ ...modules.filter((m2) => cited.has(m2)),
15966
+ ...modules.filter((m2) => !cited.has(m2))
15967
+ ]) {
15364
15968
  const g = moduleGroup(m);
15365
15969
  const list = files.get(g) ?? [];
15366
- if (list.length < 8) list.push(m);
15970
+ if (list.length < 12) list.push(m);
15367
15971
  files.set(g, list);
15368
15972
  }
15973
+ const spanning = model.features.flatMap((f) => {
15974
+ const groups = [...new Set(f.locations.map(moduleGroup))];
15975
+ return groups.length >= 2 ? [`- ${f.title}: ${groups.join(", ")}`] : [];
15976
+ });
15369
15977
  return [
15370
15978
  archNotesInput(model),
15979
+ ...spanning.length ? [
15980
+ "",
15981
+ "## Features spanning groups (each feature's code lives in all of these groups)",
15982
+ "",
15983
+ ...spanning
15984
+ ] : [],
15371
15985
  "",
15372
15986
  "## Files (the ONLY paths you may cite as evidence, copied exactly)",
15373
15987
  ...[...files.entries()].flatMap(([g, list]) => [
@@ -15632,45 +16246,69 @@ async function generateDokuInner(brainId, opts = {}) {
15632
16246
  }
15633
16247
  continue;
15634
16248
  }
15635
- const sameFacts = stored !== void 0 && state.factHash === factHash;
16249
+ const facts = aiFacts(page.kind, page.model);
16250
+ const sameFacts = stored !== void 0 && (facts !== null ? aiFacts(page.kind, stored) === facts : state.factHash === factHash);
15636
16251
  let markdown = page.markdown;
15637
16252
  let model = page.model;
15638
16253
  let icon = null;
15639
16254
  let ok2 = !required;
15640
- if (opts.llm && page.polishable && page.kind === "architecture") {
15641
- try {
15642
- const { notes } = parseArchNotes(
15643
- await opts.llm.complete({
15644
- system: ARCH_SYSTEM,
15645
- prompt: archNotesInput(doku),
15646
- maxTokens: 800
15647
- }),
15648
- doku.architecture.groups.map((g) => g.name)
15649
- );
15650
- if (notes) {
15651
- model = { ...model, narrative: notes.narrative, roles: notes.roles };
15652
- polished = true;
15653
- ok2 = true;
16255
+ if (page.polishable && page.kind === "architecture") {
16256
+ const inputs = {
16257
+ notes: sha1(archNotesInput(doku)),
16258
+ flows: sha1(archFlowsInput(doku))
16259
+ };
16260
+ const s = stored ?? {};
16261
+ let notesFor;
16262
+ let flowsFor;
16263
+ if (typeof s.narrative === "string" && s.aiInputs?.notes === inputs.notes) {
16264
+ model = { ...model, narrative: s.narrative, roles: s.roles };
16265
+ notesFor = inputs.notes;
16266
+ ok2 = true;
16267
+ } else if (opts.llm) {
16268
+ try {
16269
+ const { notes } = parseArchNotes(
16270
+ await opts.llm.complete({
16271
+ system: ARCH_SYSTEM,
16272
+ prompt: archNotesInput(doku),
16273
+ maxTokens: 800
16274
+ }),
16275
+ doku.architecture.groups.map((g) => g.name)
16276
+ );
16277
+ if (notes) {
16278
+ model = {
16279
+ ...model,
16280
+ narrative: notes.narrative,
16281
+ roles: notes.roles
16282
+ };
16283
+ notesFor = inputs.notes;
16284
+ polished = true;
16285
+ ok2 = true;
16286
+ }
16287
+ } catch {
15654
16288
  }
15655
- } catch {
15656
16289
  }
15657
- try {
15658
- const { flows } = parseArchFlows(
15659
- await opts.llm.complete({
15660
- system: ARCH_FLOWS_SYSTEM,
15661
- prompt: archFlowsInput(doku),
15662
- maxTokens: 4500
15663
- }),
15664
- new Set(doku.raw?.modules ?? [])
15665
- );
15666
- if (flows.length > 0) {
15667
- model = { ...model, flows };
15668
- polished = true;
16290
+ if (Array.isArray(s.flows) && s.flows.length > 0 && s.aiInputs?.flows === inputs.flows) {
16291
+ model = { ...model, flows: s.flows };
16292
+ flowsFor = inputs.flows;
16293
+ } else if (opts.llm) {
16294
+ try {
16295
+ const { flows } = parseArchFlows(
16296
+ await opts.llm.complete({
16297
+ system: ARCH_FLOWS_SYSTEM,
16298
+ prompt: archFlowsInput(doku),
16299
+ maxTokens: 4500
16300
+ }),
16301
+ new Set(doku.raw?.modules ?? [])
16302
+ );
16303
+ if (flows.length > 0) {
16304
+ model = { ...model, flows };
16305
+ flowsFor = inputs.flows;
16306
+ polished = true;
16307
+ }
16308
+ } catch {
15669
16309
  }
15670
- } catch {
15671
16310
  }
15672
16311
  if (!ok2 && sameFacts) {
15673
- const s = stored;
15674
16312
  const kept = model;
15675
16313
  model = {
15676
16314
  ...model,
@@ -15678,6 +16316,14 @@ async function generateDokuInner(brainId, opts = {}) {
15678
16316
  ...!kept.flows && Array.isArray(s.flows) ? { flows: s.flows } : {}
15679
16317
  };
15680
16318
  }
16319
+ const present = model;
16320
+ model = {
16321
+ ...model,
16322
+ aiInputs: {
16323
+ ...typeof present.narrative === "string" ? { notes: notesFor ?? s.aiInputs?.notes } : {},
16324
+ ...Array.isArray(present.flows) ? { flows: flowsFor ?? s.aiInputs?.flows } : {}
16325
+ }
16326
+ };
15681
16327
  const arch = model;
15682
16328
  if (arch.narrative || arch.flows?.length)
15683
16329
  markdown = renderArchitecturePage(doku, {
@@ -15686,7 +16332,11 @@ async function generateDokuInner(brainId, opts = {}) {
15686
16332
  });
15687
16333
  } else if (page.kind === "entry") {
15688
16334
  model = { ...page.model, categories };
15689
- if (opts.llm && page.polishable) {
16335
+ const s = stored;
16336
+ if (sameFacts && typeof s?.narrative === "string" && s.narrative.trim()) {
16337
+ model = { ...model, narrative: s.narrative };
16338
+ ok2 = true;
16339
+ } else if (opts.llm && page.polishable) {
15690
16340
  try {
15691
16341
  const narrative2 = (await opts.llm.complete({
15692
16342
  system: ENTRY_NARRATIVE_SYSTEM,
@@ -15700,15 +16350,16 @@ async function generateDokuInner(brainId, opts = {}) {
15700
16350
  }
15701
16351
  } catch {
15702
16352
  }
15703
- if (!ok2 && sameFacts) {
15704
- const s = stored;
15705
- if (typeof s.narrative === "string" && s.narrative.trim())
15706
- model = { ...model, narrative: s.narrative };
15707
- }
15708
16353
  }
15709
16354
  const narrative = model.narrative;
15710
16355
  if (narrative)
15711
16356
  markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
16357
+ } else if (page.kind === "feature" && sameFacts && !storedCollisions.has(page.slug) && aiComplete("feature", stored)) {
16358
+ const f = withStoredAi(page.model, stored);
16359
+ model = f;
16360
+ markdown = renderFeaturePage(f);
16361
+ if (f.docTitle) page.title = f.docTitle;
16362
+ ok2 = true;
15712
16363
  } else if (opts.llm && page.polishable) {
15713
16364
  try {
15714
16365
  const { flow } = parseFlow(
@@ -15762,25 +16413,13 @@ async function generateDokuInner(brainId, opts = {}) {
15762
16413
  hasFlowDiagram: Boolean(diagrams.flowDiagram),
15763
16414
  hasFlow: Boolean(diagrams.flow)
15764
16415
  };
15765
- const prompt = [
15766
- "Categories (pick exactly one key):",
15767
- ...categories.length ? categories.map((c) => `- ${c.key}: ${c.name} (${c.description})`) : ["- general: General (everything)"],
15768
- "",
15769
- "Available diagrams (the only allowed flow block sources):",
15770
- ...ctx.hasFlowDiagram ? [
15771
- `- flowDiagram: "${diagrams.flowDiagram.title}" (the feature's runtime flow, lanes and stations)`
15772
- ] : [],
15773
- ...ctx.hasFlow && !ctx.hasFlowDiagram ? ["- flow: the feature's runtime steps as a simple chain"] : [],
15774
- ...!ctx.hasFlowDiagram && !ctx.hasFlow ? ["- (none)"] : [],
15775
- "",
15776
- ...sharedLines.length ? [
15777
- "Documented elsewhere for the same endpoint (proven facts; reuse them, link the page):",
15778
- ...sharedLines,
15779
- ""
15780
- ] : [],
15781
- "Fact sheet:",
15782
- page.markdown
15783
- ].join("\n");
16416
+ const prompt = pagePrompt({
16417
+ categories,
16418
+ factSheet: page.markdown,
16419
+ flowDiagramTitle: diagrams.flowDiagram?.title ?? null,
16420
+ hasFlow: ctx.hasFlow,
16421
+ sharedLines
16422
+ });
15784
16423
  const keys = categoryKeys.length ? categoryKeys : ["general"];
15785
16424
  let synth = parseDocPage(
15786
16425
  await opts.llm.complete({
@@ -15810,17 +16449,20 @@ async function generateDokuInner(brainId, opts = {}) {
15810
16449
  `doku: blocks dropped (${page.slug}): ${synth.dropped.join("; ")}`
15811
16450
  );
15812
16451
  if (synth.page) {
15813
- const title = pickDocTitle(
16452
+ const s = stored;
16453
+ const same = s?.title === page.model.title && !storedCollisions.has(page.slug);
16454
+ const title = same && s?.docTitle ? s.docTitle : pickDocTitle(
15814
16455
  synth.page.title,
15815
16456
  page.title,
15816
16457
  page.slug,
15817
16458
  takenTitle
15818
16459
  );
16460
+ const category = same && s?.category && categoryKeys.includes(s.category) ? s.category : synth.page.category;
15819
16461
  const f = {
15820
16462
  ...model,
15821
16463
  presentation: synth.page.presentation,
15822
16464
  docTitle: title,
15823
- category: synth.page.category
16465
+ category
15824
16466
  };
15825
16467
  f.body = pageBody(f);
15826
16468
  model = f;
@@ -15838,17 +16480,7 @@ async function generateDokuInner(brainId, opts = {}) {
15838
16480
  console.warn(`doku: page synthesis failed (${page.slug}):`, e);
15839
16481
  }
15840
16482
  if (!ok2 && sameFacts && aiComplete("feature", stored)) {
15841
- const s = stored;
15842
- const fresh = model;
15843
- const f = {
15844
- ...model,
15845
- ...s.presentation ? { presentation: s.presentation } : {},
15846
- body: s.body ?? null,
15847
- docTitle: s.docTitle ?? null,
15848
- category: s.category ?? null,
15849
- ...!fresh.flow && s.flow ? { flow: s.flow } : {},
15850
- ...!fresh.flowDiagram && s.flowDiagram ? { flowDiagram: s.flowDiagram } : {}
15851
- };
16483
+ const f = withStoredAi(model, stored);
15852
16484
  model = f;
15853
16485
  markdown = renderFeaturePage(f);
15854
16486
  if (f.docTitle) page.title = f.docTitle;
@@ -16094,7 +16726,7 @@ var init_doku = __esm({
16094
16726
  DOKU_DIR = "brainjar-doku";
16095
16727
  MANIFEST = ".manifest.json";
16096
16728
  CHANGELOG_DAYS = 60;
16097
- sha1 = (s) => createHash2("sha1").update(s).digest("hex");
16729
+ sha1 = (s) => createHash3("sha1").update(s).digest("hex");
16098
16730
  exists = (p) => stat(p).then(
16099
16731
  () => true,
16100
16732
  () => false
@@ -16121,7 +16753,7 @@ var init_doku = __esm({
16121
16753
  `{"kind":"flow","title":"...","source":"flowDiagram"}: places the feature's existing runtime diagram where it helps the explanation; allowed only when the input lists that diagram as available, at most once.`,
16122
16754
  "Composition: explanation and outcome before technical detail. A feature exposing an API documents what it is, why you need it, and each endpoint with its parameters, request and response shape, and error cases, all from the facts only. A user-facing feature explains what is shown and how to use it, with steps only for a real action sequence. A pipeline explains what goes in, what comes out, and when it runs. A configuration-heavy feature tabulates its settings and explains precedence and effects. A feature containing several of these covers each. Mix kinds freely; there is no fixed template and no fixed number of blocks.",
16123
16755
  "Length follows the subject: an extensive feature gets several text sections with subheadings, the relationships, prerequisites, special cases, and reasons explained, and a concrete example; a small feature stays short. Never break a coherent explanation into bullet points for layout reasons and never cut it short. Explain a term at its first use; state the concrete consequence for the reader.",
16124
- "Keep every fact; invent nothing. Missing facts stay missing: leave optional blocks out, and when an indispensable piece of information is not available, say so in one plain sentence addressed to the reader (for example: The exact request path is not documented yet.), never mentioning a fact sheet, an input, or a model. Do not repeat the change history or the decision list (both are shown separately). No H1 heading (the title renders above).",
16756
+ "Keep every fact; invent nothing. Missing facts stay missing: leave optional blocks out, and when an indispensable piece of information is not available, say so in one plain sentence addressed to the reader (for example: The exact request path is not documented yet.), never mentioning a fact sheet, an input, or a model. The change history and the decision list are shown separately below the page: never reproduce them as a list or table. Their content still counts as facts. The page describes the CURRENT state: where a newer history entry or decision added, changed, replaced, or removed something, the text states the current state (for example that a capability no longer exists, or which mechanism replaced another), and the reasons that explain the current design belong in the explanation. When facts conflict, the newer one wins. Never replace a stated fact with what is usual elsewhere. No H1 heading (the title renders above).",
16125
16757
  "When the input lists facts documented elsewhere for the SAME endpoint or interface, use them: they are proven, so the endpoint may be shown here as well, and the other page may be linked with a short readable link text in the form [text](page:slug), copying the slug exactly. Never merge pages because their titles look alike.",
16126
16758
  PROSE_STYLE
16127
16759
  ].join(" ");
@@ -16177,11 +16809,12 @@ var init_doku = __esm({
16177
16809
  "You read one architecture overview of a software system (module groups with their features and dependencies, plus real file paths) and produce the system's main RUNTIME flows: how a request, session, or piece of data actually moves through the system.",
16178
16810
  'Respond ONLY with JSON of exactly this shape: {"flows": [{"slug": "...", "title": "...", "summary": "...", "explanation": "...", "lanes": [{"key": "...", "title": "..."}], "nodes": [{"id": "...", "lane": "...", "title": "...", "subtitle": "...", "kind": "...", "evidence": ["path"]}], "edges": [{"from": "...", "to": "...", "label": "...", "kind": "..."}]}]}. Nothing outside the JSON.',
16179
16811
  '"explanation" walks a reader through THIS flow in markdown (2-5 short paragraphs, ### subheadings where they help, at most 3500 characters): what starts it, what each phase does, what comes out. Describe an order only where the edges show it; where paths run in parallel or depend on a gate, say so instead of inventing a sequence. Name the same stations as the nodes.',
16180
- "Produce 2 to 4 flows, one per MAIN runtime path a newcomer must understand (for example: how input becomes stored data, how a query becomes an answer, how an external agent connects, how background verification runs). Cover every major path the features support; do not stop at two when the system clearly has more.",
16812
+ 'Produce up to 4 flows, one per MAIN runtime path a newcomer must understand (for example: how input becomes stored data, how a query becomes an answer, how an external agent connects, how background verification runs). Cover every major path the listed features describe and no path they do not: when they describe fewer paths, produce fewer flows, and when no feature is documented at all, respond {"flows": []}.',
16181
16813
  "lanes are 3-5 PHASES of the flow in order (intake, processing, storage, ...), titled in plain words. Never folder names.",
16182
16814
  "nodes are the runtime components in those phases: 4-10 per flow. kind is exactly one of: actor (human or external agent), surface (UI or endpoint), logic (application work), policy (a gate: auth, quota, validation), action (an outward call such as an LLM or external API), data (a store). title at most 3 words; subtitle at most 3 words (under 24 characters).",
16183
16815
  "evidence lists 1-3 file paths copied EXACTLY from the Files section that implement the node. Never invent or modify a path; omit evidence rather than guess.",
16184
16816
  'edges carry meaning: label with WHAT flows (at most 3 words, under 20 characters, never counts). kind is "primary" for the main path, "return" for a reply going back, "policy" for a gate outcome, "async" for background work.',
16817
+ "Draw an edge only where the input shows the connection: a listed dependency between the two groups, a feature whose code spans both groups (Features spanning groups), or one feature describing both steps. Never connect components because such systems usually are connected; a path the input does not show stays out of the flow.",
16185
16818
  "Ground everything in the listed features and dependencies. Invent nothing."
16186
16819
  ].join(" ");
16187
16820
  FEATURE_FLOW_SYSTEM = [
@@ -16311,10 +16944,12 @@ var init_verify_agent = __esm({
16311
16944
  init_sdk();
16312
16945
  init_src2();
16313
16946
  init_src();
16947
+ init_allowance();
16314
16948
  init_product_facts();
16315
16949
  init_doku();
16316
16950
  init_llm();
16317
16951
  init_openai();
16952
+ init_provider_rate2();
16318
16953
  init_routing();
16319
16954
  VERIFY_SYSTEM = [
16320
16955
  "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.",
@@ -16363,17 +16998,40 @@ var init_verify_poll = __esm({
16363
16998
  }
16364
16999
  });
16365
17000
 
17001
+ // packages/ingest/src/scheduler.ts
17002
+ var init_scheduler = __esm({
17003
+ "packages/ingest/src/scheduler.ts"() {
17004
+ "use strict";
17005
+ init_src2();
17006
+ init_daily_log();
17007
+ init_summaries();
17008
+ }
17009
+ });
17010
+
16366
17011
  // packages/ingest/src/job-worker.ts
16367
17012
  var init_job_worker = __esm({
16368
17013
  "packages/ingest/src/job-worker.ts"() {
16369
17014
  "use strict";
16370
17015
  init_src2();
16371
17016
  init_deepscan();
17017
+ init_doku();
17018
+ init_scheduler();
17019
+ init_summaries();
17020
+ init_verify_poll();
16372
17021
  init_routing();
17022
+ init_allowance();
16373
17023
  init_verify_start();
16374
17024
  }
16375
17025
  });
16376
17026
 
17027
+ // packages/ingest/src/job-clock.ts
17028
+ var init_job_clock = __esm({
17029
+ "packages/ingest/src/job-clock.ts"() {
17030
+ "use strict";
17031
+ init_src2();
17032
+ }
17033
+ });
17034
+
16377
17035
  // packages/ingest/src/ask-agent-model.ts
16378
17036
  var init_ask_agent_model = __esm({
16379
17037
  "packages/ingest/src/ask-agent-model.ts"() {
@@ -16381,18 +17039,10 @@ var init_ask_agent_model = __esm({
16381
17039
  init_sdk();
16382
17040
  init_src2();
16383
17041
  init_src();
17042
+ init_allowance();
16384
17043
  init_llm();
16385
17044
  init_openai();
16386
- }
16387
- });
16388
-
16389
- // packages/ingest/src/scheduler.ts
16390
- var init_scheduler = __esm({
16391
- "packages/ingest/src/scheduler.ts"() {
16392
- "use strict";
16393
- init_src2();
16394
- init_daily_log();
16395
- init_summaries();
17045
+ init_provider_rate2();
16396
17046
  }
16397
17047
  });
16398
17048
 
@@ -16415,6 +17065,7 @@ var init_src3 = __esm({
16415
17065
  init_llm();
16416
17066
  init_openai();
16417
17067
  init_routing();
17068
+ init_allowance();
16418
17069
  init_rationale_extractor();
16419
17070
  init_extraction();
16420
17071
  init_entities();
@@ -16453,6 +17104,7 @@ var init_src3 = __esm({
16453
17104
  init_verify_poll();
16454
17105
  init_verify_start();
16455
17106
  init_job_worker();
17107
+ init_job_clock();
16456
17108
  init_ask_agent_model();
16457
17109
  init_scheduler();
16458
17110
  registerConnector(
@@ -16505,7 +17157,24 @@ async function captureApi(pathAndQuery, init2 = {}) {
16505
17157
  }
16506
17158
  return body;
16507
17159
  }
16508
- var TokenError, TOKEN_HELP;
17160
+ async function uploadArtifacts(brain, sourceId, artifacts, cursor3) {
17161
+ let inserted = 0;
17162
+ for (let i = 0; i < artifacts.length; i += UPLOAD_PORTION) {
17163
+ const last = i + UPLOAD_PORTION >= artifacts.length;
17164
+ const r = await captureApi("/capture/artifacts", {
17165
+ method: "POST",
17166
+ body: {
17167
+ brain,
17168
+ sourceId,
17169
+ artifacts: artifacts.slice(i, i + UPLOAD_PORTION),
17170
+ cursor: last ? cursor3 : null
17171
+ }
17172
+ });
17173
+ inserted += r.inserted;
17174
+ }
17175
+ return inserted;
17176
+ }
17177
+ var TokenError, TOKEN_HELP, UPLOAD_PORTION;
16509
17178
  var init_api_client = __esm({
16510
17179
  "packages/cli/src/api-client.ts"() {
16511
17180
  "use strict";
@@ -16513,6 +17182,7 @@ var init_api_client = __esm({
16513
17182
  TokenError = class extends Error {
16514
17183
  };
16515
17184
  TOKEN_HELP = "BrainJar token revoked or invalid. Create a new token under Account in the web app, then run `npx brainjar init --brain <id> --token <token>` once in any project on this machine.";
17185
+ UPLOAD_PORTION = 2e3;
16516
17186
  }
16517
17187
  });
16518
17188
 
@@ -16745,7 +17415,7 @@ var init_debt = __esm({
16745
17415
  });
16746
17416
 
16747
17417
  // packages/cli/src/adapters/claude-code.ts
16748
- import { createHash as createHash3 } from "node:crypto";
17418
+ import { createHash as createHash4 } from "node:crypto";
16749
17419
  function targetOf(input) {
16750
17420
  const i = input ?? {};
16751
17421
  for (const k of ["command", "file_path", "notebook_path"]) {
@@ -16790,7 +17460,7 @@ var init_claude_code = __esm({
16790
17460
  "use strict";
16791
17461
  init_src();
16792
17462
  EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
16793
- short2 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 16);
17463
+ short2 = (s) => createHash4("sha256").update(s).digest("hex").slice(0, 16);
16794
17464
  claudeCode2 = {
16795
17465
  id: "claude-code",
16796
17466
  map(payload, now) {
@@ -16889,7 +17559,7 @@ var init_claude_code = __esm({
16889
17559
  });
16890
17560
 
16891
17561
  // packages/cli/src/adapters/cursor.ts
16892
- import { createHash as createHash4 } from "node:crypto";
17562
+ import { createHash as createHash5 } from "node:crypto";
16893
17563
  function targetOf2(input) {
16894
17564
  const i = input ?? {};
16895
17565
  for (const k of ["command", "file_path", "path", "target_file"]) {
@@ -16910,7 +17580,7 @@ var init_cursor = __esm({
16910
17580
  "packages/cli/src/adapters/cursor.ts"() {
16911
17581
  "use strict";
16912
17582
  init_src();
16913
- short3 = (s) => createHash4("sha256").update(s).digest("hex").slice(0, 16);
17583
+ short3 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 16);
16914
17584
  cursor2 = {
16915
17585
  id: "cursor",
16916
17586
  map(payload, now) {
@@ -17028,7 +17698,7 @@ var init_cursor = __esm({
17028
17698
  });
17029
17699
 
17030
17700
  // packages/cli/src/adapters/codex.ts
17031
- import { createHash as createHash5 } from "node:crypto";
17701
+ import { createHash as createHash6 } from "node:crypto";
17032
17702
  function patchFiles(patch) {
17033
17703
  const out = /* @__PURE__ */ new Set();
17034
17704
  for (const m of patch.matchAll(
@@ -17064,7 +17734,7 @@ var init_codex = __esm({
17064
17734
  "use strict";
17065
17735
  init_src();
17066
17736
  PATCH_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Edit", "Write"]);
17067
- short4 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 16);
17737
+ short4 = (s) => createHash6("sha256").update(s).digest("hex").slice(0, 16);
17068
17738
  codex2 = {
17069
17739
  id: "codex",
17070
17740
  map(payload, now) {
@@ -17161,7 +17831,7 @@ var init_codex = __esm({
17161
17831
  });
17162
17832
 
17163
17833
  // packages/cli/src/adapters/copilot.ts
17164
- import { createHash as createHash6 } from "node:crypto";
17834
+ import { createHash as createHash7 } from "node:crypto";
17165
17835
  function targetOf3(args) {
17166
17836
  if (!args || typeof args !== "object") return "";
17167
17837
  const a = args;
@@ -17194,7 +17864,7 @@ var init_copilot = __esm({
17194
17864
  "use strict";
17195
17865
  init_src();
17196
17866
  EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "create"]);
17197
- short5 = (s) => createHash6("sha256").update(s).digest("hex").slice(0, 16);
17867
+ short5 = (s) => createHash7("sha256").update(s).digest("hex").slice(0, 16);
17198
17868
  EVENT_ALIASES = {
17199
17869
  SessionStart: "sessionStart",
17200
17870
  SessionEnd: "sessionEnd",
@@ -17303,7 +17973,7 @@ var init_copilot = __esm({
17303
17973
  });
17304
17974
 
17305
17975
  // packages/cli/src/adapters/gemini.ts
17306
- import { createHash as createHash7 } from "node:crypto";
17976
+ import { createHash as createHash8 } from "node:crypto";
17307
17977
  function targetOf4(input) {
17308
17978
  const i = input ?? {};
17309
17979
  for (const k of ["command", "file_path"]) {
@@ -17325,7 +17995,7 @@ var init_gemini = __esm({
17325
17995
  "use strict";
17326
17996
  init_src();
17327
17997
  EDIT_TOOLS3 = /* @__PURE__ */ new Set(["write_file", "replace"]);
17328
- short6 = (s) => createHash7("sha256").update(s).digest("hex").slice(0, 16);
17998
+ short6 = (s) => createHash8("sha256").update(s).digest("hex").slice(0, 16);
17329
17999
  gemini2 = {
17330
18000
  id: "gemini",
17331
18001
  map(payload, now) {
@@ -17421,7 +18091,7 @@ var init_gemini = __esm({
17421
18091
  });
17422
18092
 
17423
18093
  // packages/cli/src/adapters/kiro.ts
17424
- import { createHash as createHash8 } from "node:crypto";
18094
+ import { createHash as createHash9 } from "node:crypto";
17425
18095
  function targetOf5(input) {
17426
18096
  const i = input ?? {};
17427
18097
  for (const k of ["command", "cmd", "path", "file_path", "filePath"]) {
@@ -17452,7 +18122,7 @@ var init_kiro = __esm({
17452
18122
  "use strict";
17453
18123
  init_src();
17454
18124
  EDIT_TOOLS4 = /* @__PURE__ */ new Set(["fs_write", "write", "str_replace", "fs_append"]);
17455
- short7 = (s) => createHash8("sha256").update(s).digest("hex").slice(0, 16);
18125
+ short7 = (s) => createHash9("sha256").update(s).digest("hex").slice(0, 16);
17456
18126
  EVENTS5 = {
17457
18127
  agentSpawn: "start",
17458
18128
  sessionStart: "start",
@@ -17568,7 +18238,7 @@ var init_kiro = __esm({
17568
18238
  });
17569
18239
 
17570
18240
  // packages/cli/src/adapters/opencode.ts
17571
- import { createHash as createHash9 } from "node:crypto";
18241
+ import { createHash as createHash10 } from "node:crypto";
17572
18242
  function commitShas7(text) {
17573
18243
  const out = /* @__PURE__ */ new Set();
17574
18244
  for (const m of (text ?? "").matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
@@ -17582,7 +18252,7 @@ var init_opencode = __esm({
17582
18252
  "use strict";
17583
18253
  init_src();
17584
18254
  EDIT_TOOLS5 = /* @__PURE__ */ new Set(["edit", "write"]);
17585
- short8 = (s) => createHash9("sha256").update(s).digest("hex").slice(0, 16);
18255
+ short8 = (s) => createHash10("sha256").update(s).digest("hex").slice(0, 16);
17586
18256
  opencode2 = {
17587
18257
  id: "opencode",
17588
18258
  map(payload, now) {
@@ -17682,7 +18352,7 @@ var init_opencode = __esm({
17682
18352
  });
17683
18353
 
17684
18354
  // packages/cli/src/adapters/cline.ts
17685
- import { createHash as createHash10 } from "node:crypto";
18355
+ import { createHash as createHash11 } from "node:crypto";
17686
18356
  function targetOf6(params) {
17687
18357
  const i = params ?? {};
17688
18358
  for (const k of ["command", "path"]) {
@@ -17714,7 +18384,7 @@ var init_cline = __esm({
17714
18384
  "use strict";
17715
18385
  init_src();
17716
18386
  EDIT_TOOLS6 = /* @__PURE__ */ new Set(["write_to_file", "replace_in_file", "apply_patch"]);
17717
- short9 = (s) => createHash10("sha256").update(s).digest("hex").slice(0, 16);
18387
+ short9 = (s) => createHash11("sha256").update(s).digest("hex").slice(0, 16);
17718
18388
  CLINE_REPLY = JSON.stringify({ cancel: false });
17719
18389
  cline2 = {
17720
18390
  id: "cline",
@@ -18016,18 +18686,15 @@ async function syncRepo(root) {
18016
18686
  const connector = createConnector("git_local", { cwd: root }, {});
18017
18687
  const artifacts = await connector.sync(project.cursor ?? void 0);
18018
18688
  if (artifacts.length > 0) {
18019
- const r = await captureApi("/capture/artifacts", {
18020
- method: "POST",
18021
- body: {
18022
- brain: link.brainId,
18023
- sourceId: project.sourceId,
18024
- artifacts,
18025
- cursor: maxUpdatedAt(artifacts) ?? project.cursor
18026
- }
18027
- });
18028
- if (r.inserted > 0) {
18689
+ const inserted = await uploadArtifacts(
18690
+ link.brainId,
18691
+ project.sourceId,
18692
+ artifacts,
18693
+ maxUpdatedAt(artifacts) ?? project.cursor
18694
+ );
18695
+ if (inserted > 0) {
18029
18696
  console.error(
18030
- `brainjar: ${r.inserted} new commits sent, the server reads them in the background`
18697
+ `brainjar: ${inserted} new commits sent, the server reads them in the background`
18031
18698
  );
18032
18699
  const graph = await scanStructure(root);
18033
18700
  await captureApi("/capture/structure", {
@@ -18095,7 +18762,7 @@ async function waitForServer(brainId, since, opts = {}) {
18095
18762
  const poll = opts.poll ?? (() => captureApi(
18096
18763
  `/capture/progress?brain=${brainId}&since=${encodeURIComponent(since.toISOString())}`
18097
18764
  ));
18098
- const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18765
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18099
18766
  const write = opts.write ?? ((s) => void process.stdout.write(s));
18100
18767
  const tty = opts.tty ?? Boolean(process.stdout.isTTY);
18101
18768
  const intervalMs = opts.intervalMs ?? 2e3;
@@ -18112,7 +18779,7 @@ async function waitForServer(brainId, since, opts = {}) {
18112
18779
  if (tty && shown) write("\n");
18113
18780
  return null;
18114
18781
  }
18115
- await sleep2(intervalMs);
18782
+ await sleep3(intervalMs);
18116
18783
  continue;
18117
18784
  }
18118
18785
  if (p.queued + p.running === 0) {
@@ -18127,7 +18794,7 @@ async function waitForServer(brainId, since, opts = {}) {
18127
18794
  shownAt = Date.now();
18128
18795
  }
18129
18796
  shown = line;
18130
- await sleep2(intervalMs);
18797
+ await sleep3(intervalMs);
18131
18798
  }
18132
18799
  }
18133
18800
  var n;
@@ -18213,16 +18880,12 @@ async function init(argv) {
18213
18880
  const artifacts = await connector.sync(conn.cursor ?? void 0);
18214
18881
  let commits = 0;
18215
18882
  if (artifacts.length > 0) {
18216
- const r = await captureApi("/capture/artifacts", {
18217
- method: "POST",
18218
- body: {
18219
- brain: conn.brainId,
18220
- sourceId: conn.sourceId,
18221
- artifacts,
18222
- cursor: maxUpdatedAt(artifacts) ?? conn.cursor
18223
- }
18224
- });
18225
- commits = r.inserted;
18883
+ commits = await uploadArtifacts(
18884
+ conn.brainId,
18885
+ conn.sourceId,
18886
+ artifacts,
18887
+ maxUpdatedAt(artifacts) ?? conn.cursor
18888
+ );
18226
18889
  }
18227
18890
  ok(
18228
18891
  `${graph.modules.length} modules, ${commits} new commits sent to the server`