brainjar 0.3.688 → 0.3.694

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 +1227 -242
  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.688";
110
+ CLI_VERSION = "0.3.694";
111
111
  CACHE = path.join(os.homedir(), ".brainjar", "version-check.json");
112
112
  DAY = 864e5;
113
113
  }
@@ -238,8 +238,8 @@ var init_github = __esm({
238
238
  if (this.opts.token) h.Authorization = `Bearer ${this.opts.token}`;
239
239
  return h;
240
240
  }
241
- async *paginate(path15, params) {
242
- let url = this.buildUrl(path15, params);
241
+ async *paginate(path19, params) {
242
+ let url = this.buildUrl(path19, params);
243
243
  while (url) {
244
244
  const res = await this.fetchImpl(url, { headers: this.headers() });
245
245
  if (!res.ok) {
@@ -250,9 +250,9 @@ var init_github = __esm({
250
250
  url = parseNextLink(res.headers.get("link"));
251
251
  }
252
252
  }
253
- buildUrl(path15, params) {
253
+ buildUrl(path19, params) {
254
254
  const url = new URL(
255
- `${this.baseUrl}/repos/${this.opts.owner}/${this.opts.repo}${path15}`
255
+ `${this.baseUrl}/repos/${this.opts.owner}/${this.opts.repo}${path19}`
256
256
  );
257
257
  url.searchParams.set("per_page", String(this.perPage));
258
258
  for (const [k, v] of Object.entries(params)) {
@@ -3735,7 +3735,7 @@ var require_split2 = __commonJS({
3735
3735
  var require_helper = __commonJS({
3736
3736
  "node_modules/pgpass/lib/helper.js"(exports, module) {
3737
3737
  "use strict";
3738
- var path15 = __require("path");
3738
+ var path19 = __require("path");
3739
3739
  var Stream2 = __require("stream").Stream;
3740
3740
  var split = require_split2();
3741
3741
  var util = __require("util");
@@ -3774,7 +3774,7 @@ var require_helper = __commonJS({
3774
3774
  };
3775
3775
  module.exports.getFileName = function(rawEnv) {
3776
3776
  var env = rawEnv || process.env;
3777
- var file = env.PGPASSFILE || (isWin ? path15.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path15.join(env.HOME || "./", ".pgpass"));
3777
+ var file = env.PGPASSFILE || (isWin ? path19.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path19.join(env.HOME || "./", ".pgpass"));
3778
3778
  return file;
3779
3779
  };
3780
3780
  module.exports.usePgPass = function(stats, fname) {
@@ -3906,7 +3906,7 @@ var require_helper = __commonJS({
3906
3906
  var require_lib = __commonJS({
3907
3907
  "node_modules/pgpass/lib/index.js"(exports, module) {
3908
3908
  "use strict";
3909
- var path15 = __require("path");
3909
+ var path19 = __require("path");
3910
3910
  var fs = __require("fs");
3911
3911
  var helper = require_helper();
3912
3912
  module.exports = function(connInfo, cb) {
@@ -5665,6 +5665,30 @@ function findEnvFile(startDir = process.cwd()) {
5665
5665
  dir = parent;
5666
5666
  }
5667
5667
  }
5668
+ function projectsPath() {
5669
+ return path2.join(os2.homedir(), ".brainjar", "projects.json");
5670
+ }
5671
+ function projectKey(root) {
5672
+ return root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
5673
+ }
5674
+ function readProjects(filePath = projectsPath()) {
5675
+ try {
5676
+ const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
5677
+ return parsed && typeof parsed === "object" ? parsed : {};
5678
+ } catch {
5679
+ return {};
5680
+ }
5681
+ }
5682
+ function projectLink(root, filePath = projectsPath()) {
5683
+ const link = readProjects(filePath)[projectKey(root)];
5684
+ return link && typeof link.brainId === "string" ? link : null;
5685
+ }
5686
+ function writeProjectLink(root, link, filePath = projectsPath()) {
5687
+ const all = readProjects(filePath);
5688
+ all[projectKey(root)] = link;
5689
+ mkdirSync2(path2.dirname(filePath), { recursive: true });
5690
+ writeFileSync2(filePath, JSON.stringify(all, null, 2) + "\n", "utf8");
5691
+ }
5668
5692
  function userConfigPath() {
5669
5693
  return path2.join(os2.homedir(), ".brainjar", "config");
5670
5694
  }
@@ -5995,17 +6019,6 @@ var init_vector = __esm({
5995
6019
  });
5996
6020
 
5997
6021
  // packages/db/src/repository.ts
5998
- async function brainForProject(root, pool2 = getPool()) {
5999
- const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
6000
- const r = await pool2.query(
6001
- `SELECT brain_id FROM source
6002
- WHERE kind = 'git_local'
6003
- AND lower(regexp_replace(replace(config->>'cwd', '\\', '/'), '/+$', '')) = $1
6004
- ORDER BY created_at DESC LIMIT 1`,
6005
- [normalized]
6006
- );
6007
- return r.rows[0]?.brain_id ?? null;
6008
- }
6009
6022
  async function getBrain(id, pool2 = getPool()) {
6010
6023
  return (await pool2.query("SELECT * FROM brain WHERE id = $1", [id])).rows[0] ?? null;
6011
6024
  }
@@ -6102,17 +6115,6 @@ var init_repository = __esm({
6102
6115
  const existing = (await this.listSources()).find((s) => s.kind === kind);
6103
6116
  return existing ?? await this.createSource(kind, name);
6104
6117
  }
6105
- /**
6106
- * The git_local source for THIS root (normalized like brainForProject), or null. One
6107
- * Brain can hold many checkouts; matching "any git_local" silently swallowed every root
6108
- * after the first (found 2026-08-21 via the benchmark's silent push).
6109
- */
6110
- async gitLocalSource(root) {
6111
- const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
6112
- return (await this.listSources()).find(
6113
- (s) => s.kind === "git_local" && String(s.config.cwd ?? "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase() === normalized
6114
- ) ?? null;
6115
- }
6116
6118
  async listSources() {
6117
6119
  return (await this.pool.query(
6118
6120
  "SELECT * FROM source WHERE brain_id = $1 ORDER BY created_at",
@@ -8850,12 +8852,12 @@ var init_path = __esm({
8850
8852
  "node_modules/@anthropic-ai/sdk/internal/utils/path.mjs"() {
8851
8853
  init_error();
8852
8854
  EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
8853
- createPathTagFunction = (pathEncoder = encodeURIPath) => function path15(statics, ...params) {
8855
+ createPathTagFunction = (pathEncoder = encodeURIPath) => function path19(statics, ...params) {
8854
8856
  if (statics.length === 1)
8855
8857
  return statics[0];
8856
8858
  let postPath = false;
8857
8859
  const invalidSegments = [];
8858
- const path16 = statics.reduce((previousValue, currentValue, index) => {
8860
+ const path20 = statics.reduce((previousValue, currentValue, index) => {
8859
8861
  if (/[?#]/.test(currentValue)) {
8860
8862
  postPath = true;
8861
8863
  }
@@ -8872,7 +8874,7 @@ var init_path = __esm({
8872
8874
  }
8873
8875
  return previousValue + currentValue + (index === params.length ? "" : encoded);
8874
8876
  }, "");
8875
- const pathOnly = path16.split(/[?#]/, 1)[0];
8877
+ const pathOnly = path20.split(/[?#]/, 1)[0];
8876
8878
  const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
8877
8879
  let match;
8878
8880
  while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
@@ -8893,10 +8895,10 @@ var init_path = __esm({
8893
8895
  }, "");
8894
8896
  throw new AnthropicError(`Path parameters result in path with invalid segments:
8895
8897
  ${invalidSegments.map((e) => e.error).join("\n")}
8896
- ${path16}
8898
+ ${path20}
8897
8899
  ${underline}`);
8898
8900
  }
8899
- return path16;
8901
+ return path20;
8900
8902
  };
8901
8903
  path3 = /* @__PURE__ */ createPathTagFunction(encodeURIPath);
8902
8904
  }
@@ -11882,9 +11884,9 @@ var init_client = __esm({
11882
11884
  makeStatusError(status, error, message, headers) {
11883
11885
  return APIError.generate(status, error, message, headers);
11884
11886
  }
11885
- buildURL(path15, query, defaultBaseURL) {
11887
+ buildURL(path19, query, defaultBaseURL) {
11886
11888
  const baseURL = !__classPrivateFieldGet(this, _BaseAnthropic_instances, "m", _BaseAnthropic_baseURLOverridden).call(this) && defaultBaseURL || this.baseURL;
11887
- const url = isAbsoluteURL(path15) ? new URL(path15) : new URL(baseURL + (baseURL.endsWith("/") && path15.startsWith("/") ? path15.slice(1) : path15));
11889
+ const url = isAbsoluteURL(path19) ? new URL(path19) : new URL(baseURL + (baseURL.endsWith("/") && path19.startsWith("/") ? path19.slice(1) : path19));
11888
11890
  const defaultQuery = this.defaultQuery();
11889
11891
  if (!isEmptyObj(defaultQuery)) {
11890
11892
  query = { ...defaultQuery, ...query };
@@ -11915,24 +11917,24 @@ var init_client = __esm({
11915
11917
  */
11916
11918
  async prepareRequest(request, { url, options }) {
11917
11919
  }
11918
- get(path15, opts) {
11919
- return this.methodRequest("get", path15, opts);
11920
+ get(path19, opts) {
11921
+ return this.methodRequest("get", path19, opts);
11920
11922
  }
11921
- post(path15, opts) {
11922
- return this.methodRequest("post", path15, opts);
11923
+ post(path19, opts) {
11924
+ return this.methodRequest("post", path19, opts);
11923
11925
  }
11924
- patch(path15, opts) {
11925
- return this.methodRequest("patch", path15, opts);
11926
+ patch(path19, opts) {
11927
+ return this.methodRequest("patch", path19, opts);
11926
11928
  }
11927
- put(path15, opts) {
11928
- return this.methodRequest("put", path15, opts);
11929
+ put(path19, opts) {
11930
+ return this.methodRequest("put", path19, opts);
11929
11931
  }
11930
- delete(path15, opts) {
11931
- return this.methodRequest("delete", path15, opts);
11932
+ delete(path19, opts) {
11933
+ return this.methodRequest("delete", path19, opts);
11932
11934
  }
11933
- methodRequest(method, path15, opts) {
11935
+ methodRequest(method, path19, opts) {
11934
11936
  return this.request(Promise.resolve(opts).then((opts2) => {
11935
- return { method, path: path15, ...opts2 };
11937
+ return { method, path: path19, ...opts2 };
11936
11938
  }));
11937
11939
  }
11938
11940
  request(options, remainingRetries = null) {
@@ -12036,8 +12038,8 @@ var init_client = __esm({
12036
12038
  }));
12037
12039
  return { response, options, controller, requestLogID, retryOfRequestLogID, startTime };
12038
12040
  }
12039
- getAPIList(path15, Page2, opts) {
12040
- return this.requestAPIList(Page2, { method: "get", path: path15, ...opts });
12041
+ getAPIList(path19, Page2, opts) {
12042
+ return this.requestAPIList(Page2, { method: "get", path: path19, ...opts });
12041
12043
  }
12042
12044
  requestAPIList(Page2, options) {
12043
12045
  const request = this.makeRequest(options, null, void 0);
@@ -12124,8 +12126,8 @@ var init_client = __esm({
12124
12126
  }
12125
12127
  async buildRequest(inputOptions, { retryCount = 0 } = {}) {
12126
12128
  const options = { ...inputOptions };
12127
- const { method, path: path15, query, defaultBaseURL } = options;
12128
- const url = this.buildURL(path15, query, defaultBaseURL);
12129
+ const { method, path: path19, query, defaultBaseURL } = options;
12130
+ const url = this.buildURL(path19, query, defaultBaseURL);
12129
12131
  if ("timeout" in options)
12130
12132
  validatePositiveInteger("timeout", options.timeout);
12131
12133
  options.timeout = options.timeout ?? this.timeout;
@@ -12602,12 +12604,25 @@ async function repoRoot(cwd = process.cwd()) {
12602
12604
  function repoName(root) {
12603
12605
  return path5.basename(root) || "project";
12604
12606
  }
12605
- async function resolveCaptureBrain(root) {
12607
+ async function mainRepoRoot(root) {
12606
12608
  try {
12607
- return await brainForProject(await repoRoot(root));
12609
+ const { stdout } = await run2(
12610
+ "git",
12611
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
12612
+ { cwd: root, windowsHide: true }
12613
+ );
12614
+ const common = path5.resolve(root, stdout.trim());
12615
+ if (path5.basename(common) === ".git") return path5.dirname(common);
12608
12616
  } catch {
12609
- return null;
12610
12617
  }
12618
+ return root;
12619
+ }
12620
+ async function captureProject(root, filePath) {
12621
+ const top = await repoRoot(root);
12622
+ return projectLink(top, filePath) ?? projectLink(await mainRepoRoot(top), filePath);
12623
+ }
12624
+ async function resolveCaptureBrain(root, filePath) {
12625
+ return (await captureProject(root, filePath))?.brainId ?? null;
12611
12626
  }
12612
12627
  async function transcriptFiles(root) {
12613
12628
  const encoded = root.replace(/[:\\/]/g, "-");
@@ -12704,22 +12719,22 @@ async function gitHooksDir(root) {
12704
12719
  return path5.join(root, ".git", "hooks");
12705
12720
  }
12706
12721
  async function installGitPostCommitHook(root) {
12707
- const hooksDir = await gitHooksDir(root);
12708
- const file = path5.join(hooksDir, "post-commit");
12722
+ const hooksDir2 = await gitHooksDir(root);
12723
+ const file = path5.join(hooksDir2, "post-commit");
12709
12724
  const existing = existsSync2(file) ? await readFile2(file, "utf8") : null;
12710
12725
  if (existing?.includes(GIT_HOOK_MARKER)) return false;
12711
12726
  const content = existing === null ? `#!/bin/sh
12712
12727
  ${gitHookBlock()}
12713
12728
  ` : existing.replace(/\n*$/, "\n") + gitHookBlock() + "\n";
12714
- await mkdir(hooksDir, { recursive: true });
12729
+ await mkdir(hooksDir2, { recursive: true });
12715
12730
  await writeFile(file, content, { mode: 493 });
12716
12731
  await chmod(file, 493).catch(() => {
12717
12732
  });
12718
12733
  return true;
12719
12734
  }
12720
12735
  async function removeGitPostCommitHook(root) {
12721
- const hooksDir = await gitHooksDir(root);
12722
- const file = path5.join(hooksDir, "post-commit");
12736
+ const hooksDir2 = await gitHooksDir(root);
12737
+ const file = path5.join(hooksDir2, "post-commit");
12723
12738
  if (!existsSync2(file)) return false;
12724
12739
  const content = await readFile2(file, "utf8");
12725
12740
  if (!content.includes(GIT_HOOK_MARKER)) return false;
@@ -12790,6 +12805,7 @@ var init_bootstrap = __esm({
12790
12805
  "packages/ingest/src/bootstrap.ts"() {
12791
12806
  "use strict";
12792
12807
  init_src2();
12808
+ init_src();
12793
12809
  init_sync();
12794
12810
  init_extraction();
12795
12811
  init_structure();
@@ -13115,14 +13131,367 @@ var init_copilot_hooks = __esm({
13115
13131
  }
13116
13132
  });
13117
13133
 
13134
+ // packages/ingest/src/gemini-hooks.ts
13135
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "node:fs/promises";
13136
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
13137
+ import path9 from "node:path";
13138
+ async function installGeminiHooks(root) {
13139
+ const file = settingsFile(root);
13140
+ let settings = {};
13141
+ if (existsSync6(file)) {
13142
+ try {
13143
+ settings = JSON.parse(await readFile6(file, "utf8"));
13144
+ } catch {
13145
+ settings = {};
13146
+ }
13147
+ }
13148
+ const hooks = settings.hooks ??= {};
13149
+ let changed = false;
13150
+ for (const { event, subcommand, probe, timeout } of EVENTS4) {
13151
+ const groups = hooks[event] ??= [];
13152
+ if (groups.some((g) => g.hooks?.some((h) => h.command?.includes(probe))))
13153
+ continue;
13154
+ groups.push({
13155
+ hooks: [
13156
+ {
13157
+ type: "command",
13158
+ command: captureHookCommand(subcommand),
13159
+ name: "brainjar",
13160
+ timeout
13161
+ }
13162
+ ]
13163
+ });
13164
+ changed = true;
13165
+ }
13166
+ if (!changed) return false;
13167
+ await mkdir5(path9.dirname(file), { recursive: true });
13168
+ await writeFile5(file, JSON.stringify(settings, null, 2) + "\n", "utf8");
13169
+ return true;
13170
+ }
13171
+ function geminiHooksInstalled(root) {
13172
+ const file = settingsFile(root);
13173
+ if (!existsSync6(file)) return false;
13174
+ try {
13175
+ return readFileSync7(file, "utf8").includes(PROBE3);
13176
+ } catch {
13177
+ return false;
13178
+ }
13179
+ }
13180
+ async function removeGeminiHooks(root) {
13181
+ const file = settingsFile(root);
13182
+ if (!existsSync6(file)) return false;
13183
+ let settings;
13184
+ try {
13185
+ settings = JSON.parse(await readFile6(file, "utf8"));
13186
+ } catch {
13187
+ return false;
13188
+ }
13189
+ const hooks = settings.hooks;
13190
+ if (!hooks) return false;
13191
+ let changed = false;
13192
+ for (const event of Object.keys(hooks)) {
13193
+ const groups = hooks[event];
13194
+ if (!Array.isArray(groups)) continue;
13195
+ const kept = groups.filter((g) => !g.hooks?.some(ours2));
13196
+ if (kept.length !== groups.length) {
13197
+ changed = true;
13198
+ if (kept.length === 0) delete hooks[event];
13199
+ else hooks[event] = kept;
13200
+ }
13201
+ }
13202
+ if (!changed) return false;
13203
+ await writeFile5(file, JSON.stringify(settings, null, 2) + "\n", "utf8");
13204
+ return true;
13205
+ }
13206
+ var PROBE3, EVENTS4, PROBES2, settingsFile, ours2;
13207
+ var init_gemini_hooks = __esm({
13208
+ "packages/ingest/src/gemini-hooks.ts"() {
13209
+ "use strict";
13210
+ init_bootstrap();
13211
+ PROBE3 = "hook gemini";
13212
+ EVENTS4 = [
13213
+ {
13214
+ event: "SessionStart",
13215
+ subcommand: "constraints",
13216
+ probe: "constraints --hook",
13217
+ timeout: 15e3
13218
+ },
13219
+ {
13220
+ event: "BeforeAgent",
13221
+ subcommand: "hook gemini",
13222
+ probe: PROBE3,
13223
+ timeout: 3e4
13224
+ },
13225
+ {
13226
+ event: "AfterTool",
13227
+ subcommand: "hook gemini",
13228
+ probe: PROBE3,
13229
+ timeout: 3e4
13230
+ },
13231
+ {
13232
+ event: "AfterAgent",
13233
+ subcommand: "hook gemini",
13234
+ probe: PROBE3,
13235
+ timeout: 12e4
13236
+ },
13237
+ {
13238
+ event: "SessionEnd",
13239
+ subcommand: "hook gemini",
13240
+ probe: PROBE3,
13241
+ timeout: 1e4
13242
+ }
13243
+ ];
13244
+ PROBES2 = EVENTS4.map((e) => e.probe);
13245
+ settingsFile = (root) => path9.join(root, ".gemini", "settings.json");
13246
+ ours2 = (h) => PROBES2.some((p) => h.command?.includes(p) ?? false);
13247
+ }
13248
+ });
13249
+
13250
+ // packages/ingest/src/kiro-hooks.ts
13251
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6, rm as rm3 } from "node:fs/promises";
13252
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
13253
+ import path10 from "node:path";
13254
+ function render2() {
13255
+ const hook = (name, trigger, command, timeout) => ({ name, trigger, action: { type: "command", command }, timeout });
13256
+ return JSON.stringify(
13257
+ {
13258
+ version: "v1",
13259
+ hooks: [
13260
+ hook(
13261
+ "brainjar-context",
13262
+ "SessionStart",
13263
+ `${captureHookCommand("constraints")} --plain`,
13264
+ 15
13265
+ ),
13266
+ hook(
13267
+ "brainjar-prompt",
13268
+ "UserPromptSubmit",
13269
+ captureHookCommand("hook kiro"),
13270
+ 30
13271
+ ),
13272
+ hook(
13273
+ "brainjar-tool",
13274
+ "PostToolUse",
13275
+ captureHookCommand("hook kiro"),
13276
+ 30
13277
+ ),
13278
+ hook("brainjar-stop", "Stop", captureHookCommand("hook kiro"), 120)
13279
+ ]
13280
+ },
13281
+ null,
13282
+ 2
13283
+ ) + "\n";
13284
+ }
13285
+ async function installKiroHooks(root) {
13286
+ const file = hooksFile4(root);
13287
+ const next = render2();
13288
+ if (existsSync7(file) && await readFile7(file, "utf8") === next) return false;
13289
+ await mkdir6(path10.dirname(file), { recursive: true });
13290
+ await writeFile6(file, next, "utf8");
13291
+ return true;
13292
+ }
13293
+ function kiroHooksInstalled(root) {
13294
+ const file = hooksFile4(root);
13295
+ if (!existsSync7(file)) return false;
13296
+ try {
13297
+ return readFileSync8(file, "utf8").includes(PROBE4);
13298
+ } catch {
13299
+ return false;
13300
+ }
13301
+ }
13302
+ async function removeKiroHooks(root) {
13303
+ if (!kiroHooksInstalled(root)) return false;
13304
+ await rm3(hooksFile4(root), { force: true });
13305
+ return true;
13306
+ }
13307
+ var PROBE4, hooksFile4;
13308
+ var init_kiro_hooks = __esm({
13309
+ "packages/ingest/src/kiro-hooks.ts"() {
13310
+ "use strict";
13311
+ init_bootstrap();
13312
+ PROBE4 = "hook kiro";
13313
+ hooksFile4 = (root) => path10.join(root, ".kiro", "hooks", "brainjar.json");
13314
+ }
13315
+ });
13316
+
13317
+ // packages/ingest/src/opencode-plugin.ts
13318
+ import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir7, rm as rm4 } from "node:fs/promises";
13319
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
13320
+ import path11 from "node:path";
13321
+ function render3() {
13322
+ const cmd = captureHookCommand("hook opencode");
13323
+ return `// BrainJar capture plugin, written by \`brainjar init\` (rewritten on every init; do not edit).
13324
+ // Ships prompts, tool calls, failures, and touched files of every OpenCode session to your Brain.
13325
+ import { spawn } from "node:child_process";
13326
+
13327
+ export const BrainJar = async ({ directory }) => {
13328
+ let session = "";
13329
+ const seen = new Set();
13330
+ const send = (payload) => {
13331
+ try {
13332
+ const child = spawn(${JSON.stringify(cmd)}, {
13333
+ shell: true,
13334
+ cwd: directory,
13335
+ stdio: ["pipe", "ignore", "ignore"],
13336
+ });
13337
+ child.on("error", () => {});
13338
+ child.stdin.end(
13339
+ JSON.stringify({ source: "opencode", cwd: directory, session_id: session, ...payload }),
13340
+ );
13341
+ } catch {}
13342
+ };
13343
+ const target = (args) => {
13344
+ if (!args || typeof args !== "object") return "";
13345
+ for (const k of ["command", "filePath", "path"]) if (typeof args[k] === "string") return args[k];
13346
+ return "";
13347
+ };
13348
+ return {
13349
+ event: async ({ event }) => {
13350
+ const p = event.properties ?? {};
13351
+ switch (event.type) {
13352
+ case "session.created":
13353
+ session = p.info?.id ?? session;
13354
+ send({ kind: "session_started" });
13355
+ break;
13356
+ case "session.idle":
13357
+ if (p.sessionID) session = p.sessionID;
13358
+ send({ kind: "turn_ended" });
13359
+ break;
13360
+ case "file.edited":
13361
+ send({ kind: "file_changed", path: p.file });
13362
+ break;
13363
+ case "message.part.updated": {
13364
+ const part = p.part;
13365
+ if (part?.type === "tool" && part.state?.status === "error" && !seen.has(part.callID)) {
13366
+ seen.add(part.callID);
13367
+ session = part.sessionID ?? session;
13368
+ send({ kind: "tool_failed", tool_name: part.tool, target: target(part.state.input), error: String(part.state.error ?? "") });
13369
+ }
13370
+ break;
13371
+ }
13372
+ }
13373
+ },
13374
+ "chat.message": async (input, output) => {
13375
+ session = input.sessionID ?? session;
13376
+ const text = (output.parts ?? []).filter((x) => x.type === "text").map((x) => x.text).join("\\n");
13377
+ if (text.trim()) send({ kind: "prompt", text });
13378
+ },
13379
+ "tool.execute.after": async (input, output) => {
13380
+ session = input.sessionID ?? session;
13381
+ send({ kind: "tool_used", tool_name: input.tool, target: target(input.args), output: String(output.output ?? "").slice(0, 4000) });
13382
+ },
13383
+ };
13384
+ };
13385
+ `;
13386
+ }
13387
+ async function installOpencodePlugin(root) {
13388
+ const file = pluginFile(root);
13389
+ const next = render3();
13390
+ if (existsSync8(file) && await readFile8(file, "utf8") === next) return false;
13391
+ await mkdir7(path11.dirname(file), { recursive: true });
13392
+ await writeFile7(file, next, "utf8");
13393
+ return true;
13394
+ }
13395
+ function opencodePluginInstalled(root) {
13396
+ const file = pluginFile(root);
13397
+ if (!existsSync8(file)) return false;
13398
+ try {
13399
+ return readFileSync9(file, "utf8").includes(PROBE5);
13400
+ } catch {
13401
+ return false;
13402
+ }
13403
+ }
13404
+ async function removeOpencodePlugin(root) {
13405
+ if (!opencodePluginInstalled(root)) return false;
13406
+ await rm4(pluginFile(root), { force: true });
13407
+ return true;
13408
+ }
13409
+ var PROBE5, pluginFile;
13410
+ var init_opencode_plugin = __esm({
13411
+ "packages/ingest/src/opencode-plugin.ts"() {
13412
+ "use strict";
13413
+ init_bootstrap();
13414
+ PROBE5 = "hook opencode";
13415
+ pluginFile = (root) => path11.join(root, ".opencode", "plugins", "brainjar.ts");
13416
+ }
13417
+ });
13418
+
13419
+ // packages/ingest/src/cline-hooks.ts
13420
+ import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir8, rm as rm5, chmod as chmod2 } from "node:fs/promises";
13421
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
13422
+ import path12 from "node:path";
13423
+ async function installClineHooks(root) {
13424
+ const dir = hooksDir(root);
13425
+ let changed = false;
13426
+ for (const { name, subcommand, flag } of SCRIPTS) {
13427
+ const file = path12.join(dir, name);
13428
+ const next = script(subcommand, flag);
13429
+ if (existsSync9(file)) {
13430
+ const current = await readFile9(file, "utf8");
13431
+ if (current === next) continue;
13432
+ if (!current.includes(MARK)) continue;
13433
+ }
13434
+ await mkdir8(dir, { recursive: true });
13435
+ await writeFile8(file, next, { mode: 493 });
13436
+ await chmod2(file, 493).catch(() => {
13437
+ });
13438
+ changed = true;
13439
+ }
13440
+ return changed;
13441
+ }
13442
+ function clineHooksInstalled(root) {
13443
+ const file = path12.join(hooksDir(root), "TaskComplete");
13444
+ if (!existsSync9(file)) return false;
13445
+ try {
13446
+ return readFileSync10(file, "utf8").includes("hook cline");
13447
+ } catch {
13448
+ return false;
13449
+ }
13450
+ }
13451
+ async function removeClineHooks(root) {
13452
+ let changed = false;
13453
+ for (const { name } of SCRIPTS) {
13454
+ const file = path12.join(hooksDir(root), name);
13455
+ if (!existsSync9(file)) continue;
13456
+ try {
13457
+ if (!readFileSync10(file, "utf8").includes(MARK)) continue;
13458
+ } catch {
13459
+ continue;
13460
+ }
13461
+ await rm5(file, { force: true });
13462
+ changed = true;
13463
+ }
13464
+ return changed;
13465
+ }
13466
+ var MARK, SCRIPTS, hooksDir, script;
13467
+ var init_cline_hooks = __esm({
13468
+ "packages/ingest/src/cline-hooks.ts"() {
13469
+ "use strict";
13470
+ init_bootstrap();
13471
+ MARK = "# brainjar-capture";
13472
+ SCRIPTS = [
13473
+ { name: "TaskStart", subcommand: "constraints", flag: " --cline" },
13474
+ { name: "UserPromptSubmit", subcommand: "hook cline", flag: "" },
13475
+ { name: "PostToolUse", subcommand: "hook cline", flag: "" },
13476
+ { name: "TaskComplete", subcommand: "hook cline", flag: "" },
13477
+ { name: "TaskCancel", subcommand: "hook cline", flag: "" }
13478
+ ];
13479
+ hooksDir = (root) => path12.join(root, ".clinerules", "hooks");
13480
+ script = (subcommand, flag) => `#!/bin/sh
13481
+ ${MARK}
13482
+ exec ${captureHookCommand(subcommand)}${flag}
13483
+ `;
13484
+ }
13485
+ });
13486
+
13118
13487
  // packages/ingest/src/tools.ts
13119
- import { existsSync as existsSync6 } from "node:fs";
13488
+ import { existsSync as existsSync10 } from "node:fs";
13120
13489
  import os4 from "node:os";
13121
- import path9 from "node:path";
13490
+ import path13 from "node:path";
13122
13491
  function detectedTools(root) {
13123
13492
  return CAPTURE_TOOLS.filter((t) => t.detect(root));
13124
13493
  }
13125
- var claudeCode, cursor, codex, copilot, CAPTURE_TOOLS;
13494
+ var claudeCode, cursor, codex, copilot, gemini, kiro, opencode, cline, CAPTURE_TOOLS;
13126
13495
  var init_tools = __esm({
13127
13496
  "packages/ingest/src/tools.ts"() {
13128
13497
  "use strict";
@@ -13130,10 +13499,14 @@ var init_tools = __esm({
13130
13499
  init_cursor_hooks();
13131
13500
  init_codex_hooks();
13132
13501
  init_copilot_hooks();
13502
+ init_gemini_hooks();
13503
+ init_kiro_hooks();
13504
+ init_opencode_plugin();
13505
+ init_cline_hooks();
13133
13506
  claudeCode = {
13134
13507
  id: "claude-code",
13135
13508
  name: "Claude Code",
13136
- detect: (root) => existsSync6(path9.join(os4.homedir(), ".claude")) || existsSync6(path9.join(root, ".claude")),
13509
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".claude")) || existsSync10(path13.join(root, ".claude")),
13137
13510
  install: installCaptureHook,
13138
13511
  installed: captureHookInstalled,
13139
13512
  remove: removeCaptureHook
@@ -13141,7 +13514,7 @@ var init_tools = __esm({
13141
13514
  cursor = {
13142
13515
  id: "cursor",
13143
13516
  name: "Cursor",
13144
- detect: (root) => existsSync6(path9.join(os4.homedir(), ".cursor")) || existsSync6(path9.join(root, ".cursor")),
13517
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".cursor")) || existsSync10(path13.join(root, ".cursor")),
13145
13518
  install: installCursorHooks,
13146
13519
  installed: cursorHooksInstalled,
13147
13520
  remove: removeCursorHooks
@@ -13149,7 +13522,7 @@ var init_tools = __esm({
13149
13522
  codex = {
13150
13523
  id: "codex",
13151
13524
  name: "Codex",
13152
- detect: (root) => existsSync6(path9.join(os4.homedir(), ".codex")) || existsSync6(path9.join(root, ".codex")),
13525
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".codex")) || existsSync10(path13.join(root, ".codex")),
13153
13526
  install: installCodexHooks,
13154
13527
  installed: codexHooksInstalled,
13155
13528
  remove: removeCodexHooks
@@ -13157,16 +13530,52 @@ var init_tools = __esm({
13157
13530
  copilot = {
13158
13531
  id: "copilot",
13159
13532
  name: "GitHub Copilot",
13160
- detect: (root) => existsSync6(path9.join(os4.homedir(), ".copilot")) || existsSync6(path9.join(root, ".github", "hooks")),
13533
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".copilot")) || existsSync10(path13.join(root, ".github", "hooks")),
13161
13534
  install: installCopilotHooks,
13162
13535
  installed: copilotHooksInstalled,
13163
13536
  remove: removeCopilotHooks
13164
13537
  };
13538
+ gemini = {
13539
+ id: "gemini",
13540
+ name: "Gemini CLI",
13541
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".gemini")) || existsSync10(path13.join(root, ".gemini")),
13542
+ install: installGeminiHooks,
13543
+ installed: geminiHooksInstalled,
13544
+ remove: removeGeminiHooks
13545
+ };
13546
+ kiro = {
13547
+ id: "kiro",
13548
+ name: "Kiro",
13549
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".kiro")) || existsSync10(path13.join(root, ".kiro")),
13550
+ install: installKiroHooks,
13551
+ installed: kiroHooksInstalled,
13552
+ remove: removeKiroHooks
13553
+ };
13554
+ opencode = {
13555
+ id: "opencode",
13556
+ name: "OpenCode",
13557
+ detect: (root) => existsSync10(path13.join(os4.homedir(), ".config", "opencode")) || existsSync10(path13.join(root, ".opencode")),
13558
+ install: installOpencodePlugin,
13559
+ installed: opencodePluginInstalled,
13560
+ remove: removeOpencodePlugin
13561
+ };
13562
+ cline = {
13563
+ id: "cline",
13564
+ name: "Cline",
13565
+ detect: (root) => existsSync10(path13.join(os4.homedir(), "Documents", "Cline")) || existsSync10(path13.join(os4.homedir(), ".cline")) || existsSync10(path13.join(root, ".clinerules")),
13566
+ install: installClineHooks,
13567
+ installed: clineHooksInstalled,
13568
+ remove: removeClineHooks
13569
+ };
13165
13570
  CAPTURE_TOOLS = [
13166
13571
  claudeCode,
13167
13572
  cursor,
13168
13573
  codex,
13169
- copilot
13574
+ copilot,
13575
+ gemini,
13576
+ kiro,
13577
+ opencode,
13578
+ cline
13170
13579
  ];
13171
13580
  }
13172
13581
  });
@@ -13643,8 +14052,8 @@ var init_doku_model = __esm({
13643
14052
 
13644
14053
  // packages/ingest/src/doku.ts
13645
14054
  import { createHash as createHash2 } from "node:crypto";
13646
- import { mkdir as mkdir5, readFile as readFile6, rm as rm3, stat, writeFile as writeFile5 } from "node:fs/promises";
13647
- import path10 from "node:path";
14055
+ import { mkdir as mkdir9, readFile as readFile10, rm as rm6, stat, writeFile as writeFile9 } from "node:fs/promises";
14056
+ import path14 from "node:path";
13648
14057
  function bodyLocations(o) {
13649
14058
  return Array.isArray(o.body.location) ? o.body.location.map(String) : [];
13650
14059
  }
@@ -13710,7 +14119,7 @@ async function loadDokuData(brainId, root) {
13710
14119
  if (root) {
13711
14120
  try {
13712
14121
  const pkg = JSON.parse(
13713
- await readFile6(path10.join(root, "package.json"), "utf8")
14122
+ await readFile10(path14.join(root, "package.json"), "utf8")
13714
14123
  );
13715
14124
  description = pkg.description ?? null;
13716
14125
  scripts = pkg.scripts ?? null;
@@ -14293,11 +14702,11 @@ function dokuFilePath(kind, slug) {
14293
14702
  return `${DOKU_DIR}/${slug}.md`;
14294
14703
  }
14295
14704
  async function writeDokuTree(root, files) {
14296
- const dir = path10.join(root, DOKU_DIR);
14297
- const manifestPath = path10.join(dir, MANIFEST);
14705
+ const dir = path14.join(root, DOKU_DIR);
14706
+ const manifestPath = path14.join(dir, MANIFEST);
14298
14707
  let old = { files: {} };
14299
14708
  try {
14300
- old = JSON.parse(await readFile6(manifestPath, "utf8"));
14709
+ old = JSON.parse(await readFile10(manifestPath, "utf8"));
14301
14710
  if (typeof old.files !== "object" || old.files === null)
14302
14711
  old = { files: {} };
14303
14712
  } catch {
@@ -14307,21 +14716,21 @@ async function writeDokuTree(root, files) {
14307
14716
  for (const file of files) {
14308
14717
  const hash = sha1(file.markdown);
14309
14718
  next[file.relPath] = { hash };
14310
- const abs = path10.join(root, file.relPath);
14719
+ const abs = path14.join(root, file.relPath);
14311
14720
  if (old.files[file.relPath]?.hash === hash && await exists(abs)) continue;
14312
- await mkdir5(path10.dirname(abs), { recursive: true });
14313
- await writeFile5(abs, file.markdown, "utf8");
14721
+ await mkdir9(path14.dirname(abs), { recursive: true });
14722
+ await writeFile9(abs, file.markdown, "utf8");
14314
14723
  written++;
14315
14724
  }
14316
14725
  let deleted = 0;
14317
14726
  for (const rel of Object.keys(old.files)) {
14318
14727
  if (next[rel]) continue;
14319
14728
  if (rel !== DOKU_FILENAME && !rel.startsWith(`${DOKU_DIR}/`)) continue;
14320
- await rm3(path10.join(root, rel), { force: true });
14729
+ await rm6(path14.join(root, rel), { force: true });
14321
14730
  deleted++;
14322
14731
  }
14323
- await mkdir5(dir, { recursive: true });
14324
- await writeFile5(
14732
+ await mkdir9(dir, { recursive: true });
14733
+ await writeFile9(
14325
14734
  manifestPath,
14326
14735
  JSON.stringify({ files: next }, null, 2),
14327
14736
  "utf8"
@@ -14474,8 +14883,8 @@ var init_ponytail_brief = __esm({
14474
14883
  });
14475
14884
 
14476
14885
  // packages/ingest/src/ponytail-debt.ts
14477
- function isDebtScannable(path15) {
14478
- return !/\.(md|mdx|markdown|txt|jsonl|log|lock)$/i.test(path15);
14886
+ function isDebtScannable(path19) {
14887
+ return !/\.(md|mdx|markdown|txt|jsonl|log|lock)$/i.test(path19);
14479
14888
  }
14480
14889
  function scanPonytailComments(files) {
14481
14890
  const out = [];
@@ -14642,6 +15051,10 @@ var init_src3 = __esm({
14642
15051
  init_cursor_hooks();
14643
15052
  init_codex_hooks();
14644
15053
  init_copilot_hooks();
15054
+ init_gemini_hooks();
15055
+ init_kiro_hooks();
15056
+ init_opencode_plugin();
15057
+ init_cline_hooks();
14645
15058
  init_commit_links();
14646
15059
  init_tool_failure();
14647
15060
  init_bootstrap();
@@ -14685,11 +15098,14 @@ function serverUrl() {
14685
15098
  ""
14686
15099
  );
14687
15100
  }
15101
+ function notConnected(root) {
15102
+ return `brainjar: no Brain is connected to ${root} on this machine. Run \`npx brainjar init --brain <id>\` there (the id is under Settings > Connect).`;
15103
+ }
14688
15104
  async function captureApi(pathAndQuery, init2 = {}) {
14689
15105
  const token = loadEnv().BRAINJAR_TOKEN?.trim();
14690
15106
  if (!token) {
14691
- throw new Error(
14692
- "BRAINJAR_TOKEN is not set \u2014 create a token in the web app (setup wizard or Settings > API tokens) and add it to ~/.brainjar/config or the project .env"
15107
+ throw new TokenError(
15108
+ "BRAINJAR_TOKEN is not set. Create a token in the web app (setup wizard or Settings > Connect) and run `npx brainjar init --brain <id> --token <token>` once on this machine."
14693
15109
  );
14694
15110
  }
14695
15111
  const res = await fetch(serverUrl() + pathAndQuery, {
@@ -14701,6 +15117,7 @@ async function captureApi(pathAndQuery, init2 = {}) {
14701
15117
  ...init2.body !== void 0 ? { body: JSON.stringify(init2.body) } : {}
14702
15118
  });
14703
15119
  const body = await res.json().catch(() => null);
15120
+ if (res.status === 401) throw new TokenError(TOKEN_HELP);
14704
15121
  if (!res.ok) {
14705
15122
  throw new Error(
14706
15123
  body?.error ?? `BrainJar server responded ${res.status} (${serverUrl()})`
@@ -14708,10 +15125,14 @@ async function captureApi(pathAndQuery, init2 = {}) {
14708
15125
  }
14709
15126
  return body;
14710
15127
  }
15128
+ var TokenError, TOKEN_HELP;
14711
15129
  var init_api_client = __esm({
14712
15130
  "packages/cli/src/api-client.ts"() {
14713
15131
  "use strict";
14714
15132
  init_src();
15133
+ TokenError = class extends Error {
15134
+ };
15135
+ 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.";
14715
15136
  }
14716
15137
  });
14717
15138
 
@@ -14727,8 +15148,8 @@ __export(brief_exports, {
14727
15148
  refreshBrief: () => refreshBrief,
14728
15149
  upsertBriefBlock: () => upsertBriefBlock
14729
15150
  });
14730
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
14731
- import path11 from "node:path";
15151
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync3, existsSync as existsSync11 } from "node:fs";
15152
+ import path15 from "node:path";
14732
15153
  function claudeMdWithImport(existing) {
14733
15154
  let text = existing;
14734
15155
  const start = text.indexOf(BRIEF_START);
@@ -14774,20 +15195,18 @@ ${BRIEF_END}
14774
15195
  async function refreshBrief(cwd) {
14775
15196
  if (loadEnv().BRAINJAR_NO_BRIEF === "1") return "skipped";
14776
15197
  const root = await repoRoot(cwd);
14777
- const project = await captureApi(
14778
- `/capture/project?root=${encodeURIComponent(root)}`
14779
- );
14780
- if (!project.brainId) return "skipped";
15198
+ const link = await captureProject(root);
15199
+ if (!link) return "skipped";
14781
15200
  const { text } = await captureApi(
14782
- `/capture/brief?brain=${project.brainId}`
15201
+ `/capture/brief?brain=${link.brainId}`
14783
15202
  );
14784
15203
  if (!text) return "skipped";
14785
- const file = path11.join(root, BRIEF_FILE);
14786
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : "";
15204
+ const file = path15.join(root, BRIEF_FILE);
15205
+ const existing = existsSync11(file) ? readFileSync11(file, "utf8") : "";
14787
15206
  const next = upsertBriefBlock(existing, text);
14788
15207
  if (next !== null) writeFileSync3(file, next);
14789
- const claudeFile = path11.join(root, "CLAUDE.md");
14790
- const claude = existsSync7(claudeFile) ? readFileSync7(claudeFile, "utf8") : "";
15208
+ const claudeFile = path15.join(root, "CLAUDE.md");
15209
+ const claude = existsSync11(claudeFile) ? readFileSync11(claudeFile, "utf8") : "";
14791
15210
  const claudeNext = claudeMdWithImport(claude);
14792
15211
  if (claudeNext !== null) writeFileSync3(claudeFile, claudeNext);
14793
15212
  return next !== null || claudeNext !== null ? "updated" : "unchanged";
@@ -14824,9 +15243,9 @@ __export(debt_exports, {
14824
15243
  scanDebtInRepo: () => scanDebtInRepo
14825
15244
  });
14826
15245
  import { execFile as execFile3 } from "node:child_process";
14827
- import { readFile as readFile7 } from "node:fs/promises";
15246
+ import { readFile as readFile11 } from "node:fs/promises";
14828
15247
  import { promisify as promisify3 } from "node:util";
14829
- import path12 from "node:path";
15248
+ import path16 from "node:path";
14830
15249
  async function scanDebtInRepo(root) {
14831
15250
  const { stdout } = await run3("git", ["-C", root, "ls-files"], {
14832
15251
  maxBuffer: 16 * 1048576
@@ -14836,7 +15255,7 @@ async function scanDebtInRepo(root) {
14836
15255
  const p = rel.trim();
14837
15256
  if (!p || !isDebtScannable(p)) continue;
14838
15257
  try {
14839
- const buf = await readFile7(path12.join(root, p));
15258
+ const buf = await readFile11(path16.join(root, p));
14840
15259
  if (buf.length > 512 * 1024 || buf.includes(0)) continue;
14841
15260
  files.push({ path: p, content: buf.toString("utf8") });
14842
15261
  } catch {
@@ -14846,14 +15265,13 @@ async function scanDebtInRepo(root) {
14846
15265
  }
14847
15266
  async function debtMain() {
14848
15267
  const root = await repoRoot(process.cwd());
14849
- const r = await captureApi(`/capture/debt?root=${encodeURIComponent(root)}`);
14850
- if (!r.brainId) {
14851
- console.error(
14852
- `brainjar: no Brain is connected to ${root} \u2014 run \`npx brainjar init\``
14853
- );
15268
+ const link = await captureProject(root);
15269
+ if (!link) {
15270
+ console.error(notConnected(root));
14854
15271
  process.exitCode = 1;
14855
15272
  return;
14856
15273
  }
15274
+ const r = await captureApi(`/capture/debt?brain=${link.brainId}`);
14857
15275
  if (r.entries.length === 0) {
14858
15276
  console.log(
14859
15277
  "No ponytail debt on record. Either disciplined or undocumented."
@@ -15262,11 +15680,11 @@ var init_codex = __esm({
15262
15680
  ...commits.length ? { commits } : {}
15263
15681
  });
15264
15682
  files.forEach(
15265
- (path15, i) => events.push({
15683
+ (path19, i) => events.push({
15266
15684
  ...base,
15267
15685
  event_id: `file:${id}:${i}`,
15268
15686
  kind: "file_changed",
15269
- path: path15
15687
+ path: path19
15270
15688
  })
15271
15689
  );
15272
15690
  }
@@ -15436,76 +15854,630 @@ var init_copilot = __esm({
15436
15854
  }
15437
15855
  });
15438
15856
 
15439
- // packages/cli/src/adapters/index.ts
15440
- var ADAPTERS;
15441
- var init_adapters = __esm({
15442
- "packages/cli/src/adapters/index.ts"() {
15443
- "use strict";
15444
- init_claude_code();
15445
- init_cursor();
15446
- init_codex();
15447
- init_copilot();
15448
- ADAPTERS = {
15449
- [claudeCode2.id]: claudeCode2,
15450
- [cursor2.id]: cursor2,
15451
- [codex2.id]: codex2,
15452
- [copilot2.id]: copilot2
15453
- };
15454
- }
15455
- });
15456
-
15457
- // packages/cli/src/hook.ts
15458
- var hook_exports = {};
15459
- __export(hook_exports, {
15460
- hookMain: () => hookMain,
15461
- parseHookArgs: () => parseHookArgs,
15462
- readSpool: () => readSpool,
15463
- spoolPath: () => spoolPath
15464
- });
15465
- import {
15466
- appendFileSync,
15467
- existsSync as existsSync8,
15468
- mkdirSync as mkdirSync3,
15469
- readdirSync,
15470
- readFileSync as readFileSync8,
15471
- rmSync,
15472
- statSync as statSync2
15473
- } from "node:fs";
15474
- import os5 from "node:os";
15475
- import path13 from "node:path";
15476
- function spoolPath(tool, sessionId) {
15477
- return path13.join(
15478
- os5.homedir(),
15479
- ".brainjar",
15480
- "spool",
15481
- tool,
15482
- `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.jsonl`
15483
- );
15484
- }
15485
- function readSpool(file) {
15486
- const out = [];
15487
- for (const line of readFileSync8(file, "utf8").split("\n")) {
15488
- if (!line.trim()) continue;
15489
- try {
15490
- out.push(clampCaptureEvent(JSON.parse(line)));
15491
- } catch {
15492
- }
15857
+ // packages/cli/src/adapters/gemini.ts
15858
+ import { createHash as createHash7 } from "node:crypto";
15859
+ function targetOf4(input) {
15860
+ const i = input ?? {};
15861
+ for (const k of ["command", "file_path"]) {
15862
+ if (typeof i[k] === "string") return i[k];
15493
15863
  }
15494
- return out;
15864
+ return "";
15495
15865
  }
15496
- function pruneSpool(dir, keep, maxAgeMs = 7 * 864e5) {
15497
- try {
15498
- for (const f of readdirSync(dir)) {
15499
- const file = path13.join(dir, f);
15500
- if (file === keep) continue;
15501
- if (Date.now() - statSync2(file).mtimeMs > maxAgeMs)
15502
- rmSync(file, { force: true });
15503
- }
15504
- } catch {
15866
+ function commitShas5(v) {
15867
+ const text = typeof v === "string" ? v : JSON.stringify(v ?? "");
15868
+ const out = /* @__PURE__ */ new Set();
15869
+ for (const m of text.matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
15870
+ out.add(m[1].toLowerCase());
15505
15871
  }
15872
+ return [...out];
15506
15873
  }
15507
- function parseHookArgs(argv) {
15508
- const eventFlag = argv.indexOf("--event");
15874
+ var EDIT_TOOLS3, short6, gemini2;
15875
+ var init_gemini = __esm({
15876
+ "packages/cli/src/adapters/gemini.ts"() {
15877
+ "use strict";
15878
+ init_src();
15879
+ EDIT_TOOLS3 = /* @__PURE__ */ new Set(["write_file", "replace"]);
15880
+ short6 = (s) => createHash7("sha256").update(s).digest("hex").slice(0, 16);
15881
+ gemini2 = {
15882
+ id: "gemini",
15883
+ map(payload, now) {
15884
+ const p = payload;
15885
+ if (!p || typeof p !== "object" || typeof p.session_id !== "string") {
15886
+ return null;
15887
+ }
15888
+ const at = typeof p.timestamp === "string" && !Number.isNaN(Date.parse(p.timestamp)) ? new Date(p.timestamp).toISOString() : now;
15889
+ const base = {
15890
+ v: CAPTURE_EVENT_VERSION,
15891
+ tool: "gemini",
15892
+ session_id: p.session_id,
15893
+ at,
15894
+ cwd: p.cwd ?? null
15895
+ };
15896
+ const events = [];
15897
+ let flush = false;
15898
+ let end = false;
15899
+ switch (p.hook_event_name) {
15900
+ case "SessionStart":
15901
+ events.push({
15902
+ ...base,
15903
+ event_id: `start:${p.session_id}`,
15904
+ kind: "session_started"
15905
+ });
15906
+ break;
15907
+ case "BeforeAgent":
15908
+ if (typeof p.prompt === "string" && p.prompt.trim()) {
15909
+ events.push({
15910
+ ...base,
15911
+ event_id: `prompt:${short6(at + p.prompt)}`,
15912
+ kind: "prompt",
15913
+ text: p.prompt
15914
+ });
15915
+ }
15916
+ break;
15917
+ case "AfterTool": {
15918
+ const tool = p.tool_name ?? "tool";
15919
+ const target = targetOf4(p.tool_input);
15920
+ const id = short6(at + tool + target);
15921
+ const err = p.tool_response?.error;
15922
+ if (err && typeof err === "object") {
15923
+ const error = String(err.message ?? err.type ?? "tool error");
15924
+ events.push({
15925
+ ...base,
15926
+ event_id: `fail:${id}`,
15927
+ kind: "tool_failed",
15928
+ tool_name: tool,
15929
+ ...target ? { target } : {},
15930
+ error
15931
+ });
15932
+ break;
15933
+ }
15934
+ const commits = commitShas5(p.tool_response?.llmContent);
15935
+ events.push({
15936
+ ...base,
15937
+ event_id: `use:${id}`,
15938
+ kind: "tool_used",
15939
+ tool_name: tool,
15940
+ ...target ? { target } : {},
15941
+ ...commits.length ? { commits } : {}
15942
+ });
15943
+ if (EDIT_TOOLS3.has(tool) && target) {
15944
+ events.push({
15945
+ ...base,
15946
+ event_id: `file:${id}`,
15947
+ kind: "file_changed",
15948
+ path: target
15949
+ });
15950
+ }
15951
+ break;
15952
+ }
15953
+ case "AfterAgent":
15954
+ flush = true;
15955
+ break;
15956
+ case "SessionEnd":
15957
+ events.push({
15958
+ ...base,
15959
+ event_id: `end:${p.session_id}`,
15960
+ kind: "session_ended",
15961
+ ...p.reason ? { reason: p.reason } : {}
15962
+ });
15963
+ flush = true;
15964
+ end = true;
15965
+ break;
15966
+ default:
15967
+ return null;
15968
+ }
15969
+ return { sessionId: p.session_id, cwd: p.cwd ?? null, events, flush, end };
15970
+ }
15971
+ };
15972
+ }
15973
+ });
15974
+
15975
+ // packages/cli/src/adapters/kiro.ts
15976
+ import { createHash as createHash8 } from "node:crypto";
15977
+ function targetOf5(input) {
15978
+ const i = input ?? {};
15979
+ for (const k of ["command", "cmd", "path", "file_path", "filePath"]) {
15980
+ if (typeof i[k] === "string") return i[k];
15981
+ }
15982
+ return "";
15983
+ }
15984
+ function commitShas6(v) {
15985
+ const text = typeof v === "string" ? v : JSON.stringify(v ?? "");
15986
+ const out = /* @__PURE__ */ new Set();
15987
+ for (const m of text.matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
15988
+ out.add(m[1].toLowerCase());
15989
+ }
15990
+ return [...out];
15991
+ }
15992
+ function failureOf2(response) {
15993
+ if (!response || typeof response !== "object") return null;
15994
+ const r = response;
15995
+ if (r.success !== false) return null;
15996
+ const text = [r.error, r.result, r.message].find(
15997
+ (t) => typeof t === "string" && t.trim()
15998
+ );
15999
+ return (typeof text === "string" ? text : JSON.stringify(r)).trim();
16000
+ }
16001
+ var EDIT_TOOLS4, short7, EVENTS5, kiro2;
16002
+ var init_kiro = __esm({
16003
+ "packages/cli/src/adapters/kiro.ts"() {
16004
+ "use strict";
16005
+ init_src();
16006
+ EDIT_TOOLS4 = /* @__PURE__ */ new Set(["fs_write", "write", "str_replace", "fs_append"]);
16007
+ short7 = (s) => createHash8("sha256").update(s).digest("hex").slice(0, 16);
16008
+ EVENTS5 = {
16009
+ agentSpawn: "start",
16010
+ sessionStart: "start",
16011
+ SessionStart: "start",
16012
+ userPromptSubmit: "prompt",
16013
+ UserPromptSubmit: "prompt",
16014
+ promptSubmit: "prompt",
16015
+ postToolUse: "tool",
16016
+ PostToolUse: "tool",
16017
+ postFileSave: "file",
16018
+ PostFileSave: "file",
16019
+ postFileCreate: "file",
16020
+ PostFileCreate: "file",
16021
+ stop: "stop",
16022
+ Stop: "stop",
16023
+ agentStop: "stop"
16024
+ };
16025
+ kiro2 = {
16026
+ id: "kiro",
16027
+ map(payload, now) {
16028
+ const p = payload;
16029
+ if (!p || typeof p !== "object" || typeof p.session_id !== "string") {
16030
+ return null;
16031
+ }
16032
+ const base = {
16033
+ v: CAPTURE_EVENT_VERSION,
16034
+ tool: "kiro",
16035
+ session_id: p.session_id,
16036
+ at: now,
16037
+ cwd: p.cwd ?? null
16038
+ };
16039
+ const events = [];
16040
+ let flush = false;
16041
+ switch (EVENTS5[p.hook_event_name ?? ""]) {
16042
+ case "start":
16043
+ events.push({
16044
+ ...base,
16045
+ event_id: `start:${p.session_id}`,
16046
+ kind: "session_started"
16047
+ });
16048
+ break;
16049
+ case "prompt":
16050
+ if (typeof p.prompt === "string" && p.prompt.trim()) {
16051
+ events.push({
16052
+ ...base,
16053
+ event_id: `prompt:${short7(now + p.prompt)}`,
16054
+ kind: "prompt",
16055
+ text: p.prompt
16056
+ });
16057
+ }
16058
+ break;
16059
+ case "tool": {
16060
+ const tool = p.tool_name ?? "tool";
16061
+ const target = targetOf5(p.tool_input);
16062
+ const id = short7(now + tool + target);
16063
+ const error = failureOf2(p.tool_response);
16064
+ if (error) {
16065
+ events.push({
16066
+ ...base,
16067
+ event_id: `fail:${id}`,
16068
+ kind: "tool_failed",
16069
+ tool_name: tool,
16070
+ ...target ? { target } : {},
16071
+ error
16072
+ });
16073
+ break;
16074
+ }
16075
+ const commits = commitShas6(p.tool_response);
16076
+ events.push({
16077
+ ...base,
16078
+ event_id: `use:${id}`,
16079
+ kind: "tool_used",
16080
+ tool_name: tool,
16081
+ ...target ? { target } : {},
16082
+ ...commits.length ? { commits } : {}
16083
+ });
16084
+ if (EDIT_TOOLS4.has(tool) && target) {
16085
+ events.push({
16086
+ ...base,
16087
+ event_id: `file:${id}`,
16088
+ kind: "file_changed",
16089
+ path: target
16090
+ });
16091
+ }
16092
+ break;
16093
+ }
16094
+ case "file":
16095
+ if (typeof p.file_path === "string" && p.file_path) {
16096
+ events.push({
16097
+ ...base,
16098
+ event_id: `file:${short7(now + p.file_path)}`,
16099
+ kind: "file_changed",
16100
+ path: p.file_path
16101
+ });
16102
+ }
16103
+ break;
16104
+ case "stop":
16105
+ flush = true;
16106
+ break;
16107
+ default:
16108
+ return null;
16109
+ }
16110
+ return {
16111
+ sessionId: p.session_id,
16112
+ cwd: p.cwd ?? null,
16113
+ events,
16114
+ flush,
16115
+ end: false
16116
+ };
16117
+ }
16118
+ };
16119
+ }
16120
+ });
16121
+
16122
+ // packages/cli/src/adapters/opencode.ts
16123
+ import { createHash as createHash9 } from "node:crypto";
16124
+ function commitShas7(text) {
16125
+ const out = /* @__PURE__ */ new Set();
16126
+ for (const m of (text ?? "").matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
16127
+ out.add(m[1].toLowerCase());
16128
+ }
16129
+ return [...out];
16130
+ }
16131
+ var EDIT_TOOLS5, short8, opencode2;
16132
+ var init_opencode = __esm({
16133
+ "packages/cli/src/adapters/opencode.ts"() {
16134
+ "use strict";
16135
+ init_src();
16136
+ EDIT_TOOLS5 = /* @__PURE__ */ new Set(["edit", "write"]);
16137
+ short8 = (s) => createHash9("sha256").update(s).digest("hex").slice(0, 16);
16138
+ opencode2 = {
16139
+ id: "opencode",
16140
+ map(payload, now) {
16141
+ const p = payload;
16142
+ if (!p || typeof p !== "object" || p.source !== "opencode" || typeof p.session_id !== "string" || !p.session_id) {
16143
+ return null;
16144
+ }
16145
+ const base = {
16146
+ v: CAPTURE_EVENT_VERSION,
16147
+ tool: "opencode",
16148
+ session_id: p.session_id,
16149
+ at: now,
16150
+ cwd: p.cwd ?? null
16151
+ };
16152
+ const events = [];
16153
+ let flush = false;
16154
+ const target = p.target?.trim() ?? "";
16155
+ const tool = p.tool_name ?? "tool";
16156
+ switch (p.kind) {
16157
+ case "session_started":
16158
+ events.push({
16159
+ ...base,
16160
+ event_id: `start:${p.session_id}`,
16161
+ kind: "session_started"
16162
+ });
16163
+ break;
16164
+ case "prompt":
16165
+ if (typeof p.text === "string" && p.text.trim()) {
16166
+ events.push({
16167
+ ...base,
16168
+ event_id: `prompt:${short8(now + p.text)}`,
16169
+ kind: "prompt",
16170
+ text: p.text
16171
+ });
16172
+ }
16173
+ break;
16174
+ case "tool_used": {
16175
+ const id = short8(now + tool + target);
16176
+ const commits = commitShas7(p.output);
16177
+ events.push({
16178
+ ...base,
16179
+ event_id: `use:${id}`,
16180
+ kind: "tool_used",
16181
+ tool_name: tool,
16182
+ ...target ? { target } : {},
16183
+ ...commits.length ? { commits } : {}
16184
+ });
16185
+ if (EDIT_TOOLS5.has(tool) && target) {
16186
+ events.push({
16187
+ ...base,
16188
+ event_id: `file:${id}`,
16189
+ kind: "file_changed",
16190
+ path: target
16191
+ });
16192
+ }
16193
+ break;
16194
+ }
16195
+ case "tool_failed": {
16196
+ const error = (p.error ?? "").trim();
16197
+ if (!error) break;
16198
+ events.push({
16199
+ ...base,
16200
+ event_id: `fail:${short8(now + tool + target + error)}`,
16201
+ kind: "tool_failed",
16202
+ tool_name: tool,
16203
+ ...target ? { target } : {},
16204
+ error
16205
+ });
16206
+ break;
16207
+ }
16208
+ case "file_changed":
16209
+ if (typeof p.path === "string" && p.path) {
16210
+ events.push({
16211
+ ...base,
16212
+ event_id: `file:${short8(now + p.path)}`,
16213
+ kind: "file_changed",
16214
+ path: p.path
16215
+ });
16216
+ }
16217
+ break;
16218
+ case "turn_ended":
16219
+ flush = true;
16220
+ break;
16221
+ default:
16222
+ return null;
16223
+ }
16224
+ return {
16225
+ sessionId: p.session_id,
16226
+ cwd: p.cwd ?? null,
16227
+ events,
16228
+ flush,
16229
+ end: false
16230
+ };
16231
+ }
16232
+ };
16233
+ }
16234
+ });
16235
+
16236
+ // packages/cli/src/adapters/cline.ts
16237
+ import { createHash as createHash10 } from "node:crypto";
16238
+ function targetOf6(params) {
16239
+ const i = params ?? {};
16240
+ for (const k of ["command", "path"]) {
16241
+ if (typeof i[k] === "string") return i[k];
16242
+ }
16243
+ return "";
16244
+ }
16245
+ function commitShas8(text) {
16246
+ const out = /* @__PURE__ */ new Set();
16247
+ for (const m of (text ?? "").matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
16248
+ out.add(m[1].toLowerCase());
16249
+ }
16250
+ return [...out];
16251
+ }
16252
+ function stamp2(ts, now) {
16253
+ const n = typeof ts === "string" && /^\d+$/.test(ts) ? Number(ts) : ts;
16254
+ if (typeof n === "number" && Number.isFinite(n))
16255
+ return new Date(n).toISOString();
16256
+ if (typeof ts === "string" && !Number.isNaN(Date.parse(ts)))
16257
+ return new Date(ts).toISOString();
16258
+ return now;
16259
+ }
16260
+ function clineContextEnvelope(text) {
16261
+ return JSON.stringify({ cancel: false, contextModification: text });
16262
+ }
16263
+ var EDIT_TOOLS6, short9, CLINE_REPLY, cline2;
16264
+ var init_cline = __esm({
16265
+ "packages/cli/src/adapters/cline.ts"() {
16266
+ "use strict";
16267
+ init_src();
16268
+ EDIT_TOOLS6 = /* @__PURE__ */ new Set(["write_to_file", "replace_in_file", "apply_patch"]);
16269
+ short9 = (s) => createHash10("sha256").update(s).digest("hex").slice(0, 16);
16270
+ CLINE_REPLY = JSON.stringify({ cancel: false });
16271
+ cline2 = {
16272
+ id: "cline",
16273
+ map(payload, now) {
16274
+ const p = payload;
16275
+ if (!p || typeof p !== "object" || typeof p.taskId !== "string") {
16276
+ return null;
16277
+ }
16278
+ const at = stamp2(p.timestamp, now);
16279
+ const cwd = p.workspaceRoots?.[0] ?? null;
16280
+ const base = {
16281
+ v: CAPTURE_EVENT_VERSION,
16282
+ tool: "cline",
16283
+ session_id: p.taskId,
16284
+ at,
16285
+ cwd
16286
+ };
16287
+ const events = [];
16288
+ let flush = false;
16289
+ let end = false;
16290
+ switch (p.hookName) {
16291
+ case "TaskStart":
16292
+ case "TaskResume":
16293
+ events.push({
16294
+ ...base,
16295
+ event_id: `start:${p.taskId}`,
16296
+ kind: "session_started"
16297
+ });
16298
+ break;
16299
+ case "UserPromptSubmit": {
16300
+ const prompt = p.userPromptSubmit?.prompt;
16301
+ if (typeof prompt === "string" && prompt.trim()) {
16302
+ events.push({
16303
+ ...base,
16304
+ event_id: `prompt:${short9(at + prompt)}`,
16305
+ kind: "prompt",
16306
+ text: prompt
16307
+ });
16308
+ }
16309
+ break;
16310
+ }
16311
+ case "PostToolUse": {
16312
+ const t = p.postToolUse ?? {};
16313
+ const tool = t.toolName ?? "tool";
16314
+ const target = targetOf6(t.parameters);
16315
+ const id = short9(at + tool + target);
16316
+ if (t.success !== true) {
16317
+ const error = (t.result ?? "").trim() || "tool failed";
16318
+ events.push({
16319
+ ...base,
16320
+ event_id: `fail:${id}`,
16321
+ kind: "tool_failed",
16322
+ tool_name: tool,
16323
+ ...target ? { target } : {},
16324
+ error
16325
+ });
16326
+ break;
16327
+ }
16328
+ const commits = commitShas8(t.result);
16329
+ events.push({
16330
+ ...base,
16331
+ event_id: `use:${id}`,
16332
+ kind: "tool_used",
16333
+ tool_name: tool,
16334
+ ...target ? { target } : {},
16335
+ ...commits.length ? { commits } : {}
16336
+ });
16337
+ if (EDIT_TOOLS6.has(tool) && target) {
16338
+ events.push({
16339
+ ...base,
16340
+ event_id: `file:${id}`,
16341
+ kind: "file_changed",
16342
+ path: target
16343
+ });
16344
+ }
16345
+ break;
16346
+ }
16347
+ case "TaskComplete":
16348
+ case "TaskCancel":
16349
+ events.push({
16350
+ ...base,
16351
+ event_id: `end:${p.taskId}`,
16352
+ kind: "session_ended",
16353
+ reason: p.hookName === "TaskCancel" ? "cancelled" : "completed"
16354
+ });
16355
+ flush = true;
16356
+ end = true;
16357
+ break;
16358
+ default:
16359
+ return null;
16360
+ }
16361
+ return {
16362
+ sessionId: p.taskId,
16363
+ cwd,
16364
+ events,
16365
+ flush,
16366
+ end,
16367
+ reply: CLINE_REPLY
16368
+ };
16369
+ }
16370
+ };
16371
+ }
16372
+ });
16373
+
16374
+ // packages/cli/src/adapters/index.ts
16375
+ var ADAPTERS;
16376
+ var init_adapters = __esm({
16377
+ "packages/cli/src/adapters/index.ts"() {
16378
+ "use strict";
16379
+ init_claude_code();
16380
+ init_cursor();
16381
+ init_codex();
16382
+ init_copilot();
16383
+ init_gemini();
16384
+ init_kiro();
16385
+ init_opencode();
16386
+ init_cline();
16387
+ ADAPTERS = {
16388
+ [claudeCode2.id]: claudeCode2,
16389
+ [cursor2.id]: cursor2,
16390
+ [codex2.id]: codex2,
16391
+ [copilot2.id]: copilot2,
16392
+ [gemini2.id]: gemini2,
16393
+ [kiro2.id]: kiro2,
16394
+ [opencode2.id]: opencode2,
16395
+ [cline2.id]: cline2
16396
+ };
16397
+ }
16398
+ });
16399
+
16400
+ // packages/cli/src/hook.ts
16401
+ var hook_exports = {};
16402
+ __export(hook_exports, {
16403
+ hookMain: () => hookMain,
16404
+ parseHookArgs: () => parseHookArgs,
16405
+ readSpool: () => readSpool,
16406
+ spoolPath: () => spoolPath,
16407
+ staleSpools: () => staleSpools
16408
+ });
16409
+ import {
16410
+ appendFileSync,
16411
+ existsSync as existsSync12,
16412
+ mkdirSync as mkdirSync3,
16413
+ readdirSync,
16414
+ readFileSync as readFileSync12,
16415
+ rmSync,
16416
+ statSync as statSync2
16417
+ } from "node:fs";
16418
+ import os5 from "node:os";
16419
+ import path17 from "node:path";
16420
+ function spoolPath(tool, sessionId) {
16421
+ return path17.join(
16422
+ os5.homedir(),
16423
+ ".brainjar",
16424
+ "spool",
16425
+ tool,
16426
+ `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.jsonl`
16427
+ );
16428
+ }
16429
+ function readSpool(file) {
16430
+ const out = [];
16431
+ for (const line of readFileSync12(file, "utf8").split("\n")) {
16432
+ if (!line.trim()) continue;
16433
+ try {
16434
+ out.push(clampCaptureEvent(JSON.parse(line)));
16435
+ } catch {
16436
+ }
16437
+ }
16438
+ return out;
16439
+ }
16440
+ function pruneSpool(dir, keep, maxAgeMs = 7 * 864e5) {
16441
+ try {
16442
+ for (const f of readdirSync(dir)) {
16443
+ const file = path17.join(dir, f);
16444
+ if (file === keep) continue;
16445
+ if (Date.now() - statSync2(file).mtimeMs > maxAgeMs)
16446
+ rmSync(file, { force: true });
16447
+ }
16448
+ } catch {
16449
+ }
16450
+ }
16451
+ function staleSpools(dir, keep, minAgeMs = 36e5) {
16452
+ try {
16453
+ return readdirSync(dir).map((f) => path17.join(dir, f)).filter(
16454
+ (file) => file !== keep && Date.now() - statSync2(file).mtimeMs > minAgeMs
16455
+ ).sort();
16456
+ } catch {
16457
+ return [];
16458
+ }
16459
+ }
16460
+ async function linkForSpool(file) {
16461
+ const cwd = readSpool(file).find((e) => e.cwd)?.cwd;
16462
+ return cwd ? captureProject(await repoRoot(cwd)) : null;
16463
+ }
16464
+ async function shipSpool(file, link) {
16465
+ const events = readSpool(file);
16466
+ if (!events.length) return;
16467
+ const r = await captureApi("/capture/events", {
16468
+ method: "POST",
16469
+ body: {
16470
+ brain: link.brainId,
16471
+ events,
16472
+ author: loadEnv().BRAINJAR_AUTHOR?.trim() || null
16473
+ }
16474
+ });
16475
+ console.error(
16476
+ `brainjar: session captured \u2014 ${r.prompts ?? 0} intents, ${r.failures ?? 0} failures, ${(r.skippedNoise ?? 0) + (r.skippedFailures ?? 0)} noise skipped` + (r.consolidated ? `, ${r.consolidated} product facts` : "")
16477
+ );
16478
+ }
16479
+ function parseHookArgs(argv) {
16480
+ const eventFlag = argv.indexOf("--event");
15509
16481
  const eventIndex = eventFlag >= 0 ? eventFlag + 1 : -1;
15510
16482
  const event = eventIndex >= 0 ? argv[eventIndex] : void 0;
15511
16483
  const tool = argv.find((a, i) => !a.startsWith("--") && i !== eventIndex);
@@ -15523,7 +16495,7 @@ async function hookMain(argv) {
15523
16495
  }
15524
16496
  let payload = null;
15525
16497
  try {
15526
- payload = JSON.parse(readFileSync8(0, "utf8"));
16498
+ payload = JSON.parse(readFileSync12(0, "utf8"));
15527
16499
  } catch {
15528
16500
  return;
15529
16501
  }
@@ -15532,7 +16504,7 @@ async function hookMain(argv) {
15532
16504
  if (out.reply) process.stdout.write(out.reply);
15533
16505
  const file = spoolPath(adapter.id, out.sessionId);
15534
16506
  if (out.events.length) {
15535
- mkdirSync3(path13.dirname(file), { recursive: true });
16507
+ mkdirSync3(path17.dirname(file), { recursive: true });
15536
16508
  appendFileSync(
15537
16509
  file,
15538
16510
  out.events.map((e) => JSON.stringify(clampCaptureEvent(e))).join("\n") + "\n"
@@ -15540,28 +16512,22 @@ async function hookMain(argv) {
15540
16512
  }
15541
16513
  if (!out.flush) return;
15542
16514
  const root = await repoRoot(out.cwd ?? process.cwd());
15543
- const events = existsSync8(file) ? readSpool(file) : [];
15544
- if (events.length) {
15545
- const r = await captureApi("/capture/events", {
15546
- method: "POST",
15547
- body: {
15548
- root,
15549
- events,
15550
- author: loadEnv().BRAINJAR_AUTHOR?.trim() || null
15551
- }
15552
- });
15553
- if (!r.brainId) {
15554
- console.error(
15555
- `brainjar: no Brain is connected to ${root} \u2014 run \`npx brainjar init\` (skipping capture)`
15556
- );
15557
- } else {
15558
- console.error(
15559
- `brainjar: session captured \u2014 ${r.prompts ?? 0} intents, ${r.failures ?? 0} failures, ${(r.skippedNoise ?? 0) + (r.skippedFailures ?? 0)} noise skipped` + (r.consolidated ? `, ${r.consolidated} product facts` : "")
15560
- );
15561
- }
16515
+ const link = await captureProject(root);
16516
+ if (!link) {
16517
+ console.error(notConnected(root));
16518
+ if (out.end) rmSync(file, { force: true });
16519
+ return;
15562
16520
  }
16521
+ if (existsSync12(file)) await shipSpool(file, link);
15563
16522
  if (out.end) rmSync(file, { force: true });
15564
- pruneSpool(path13.dirname(file), file);
16523
+ const dir = path17.dirname(file);
16524
+ for (const other of staleSpools(dir, file)) {
16525
+ const otherLink = await linkForSpool(other);
16526
+ if (!otherLink) continue;
16527
+ await shipSpool(other, otherLink);
16528
+ rmSync(other, { force: true });
16529
+ }
16530
+ pruneSpool(dir, file);
15565
16531
  await syncRepo(root);
15566
16532
  } catch (e) {
15567
16533
  console.error("brainjar hook:", e instanceof Error ? e.message : e);
@@ -15596,13 +16562,12 @@ function capTranscript(text) {
15596
16562
  }
15597
16563
  async function syncRepo(root) {
15598
16564
  try {
15599
- const project = await captureApi(`/capture/project?root=${encodeURIComponent(root)}`);
15600
- if (!project.brainId) {
15601
- console.error(
15602
- `brainjar: no Brain is connected to ${root} \u2014 run \`npx brainjar init\` (skipping capture)`
15603
- );
16565
+ const link = await captureProject(root);
16566
+ if (!link) {
16567
+ console.error(notConnected(root));
15604
16568
  return;
15605
16569
  }
16570
+ const project = await captureApi(`/capture/project?brain=${link.brainId}&source=${link.sourceId}`);
15606
16571
  if (project.sourceId) {
15607
16572
  const connector = createConnector("git_local", { cwd: root }, {});
15608
16573
  const artifacts = await connector.sync(project.cursor ?? void 0);
@@ -15612,7 +16577,7 @@ async function syncRepo(root) {
15612
16577
  {
15613
16578
  method: "POST",
15614
16579
  body: {
15615
- root,
16580
+ brain: link.brainId,
15616
16581
  sourceId: project.sourceId,
15617
16582
  artifacts,
15618
16583
  cursor: maxUpdatedAt(artifacts) ?? project.cursor
@@ -15627,7 +16592,7 @@ async function syncRepo(root) {
15627
16592
  const graph = await scanStructure(root);
15628
16593
  await captureApi("/capture/structure", {
15629
16594
  method: "POST",
15630
- body: { root, graph }
16595
+ body: { brain: link.brainId, graph }
15631
16596
  });
15632
16597
  }
15633
16598
  }
@@ -15637,7 +16602,10 @@ async function syncRepo(root) {
15637
16602
  const entries = await scanDebtInRepo2(root);
15638
16603
  const r = await captureApi(
15639
16604
  "/capture/debt",
15640
- { method: "POST", body: { root, entries } }
16605
+ {
16606
+ method: "POST",
16607
+ body: { brain: link.brainId, source: link.sourceId, entries }
16608
+ }
15641
16609
  );
15642
16610
  if (r.added || r.resolved)
15643
16611
  console.error(
@@ -15684,7 +16652,7 @@ __export(init_exports, {
15684
16652
  init: () => init,
15685
16653
  initMain: () => initMain
15686
16654
  });
15687
- import { readFile as readFile8 } from "node:fs/promises";
16655
+ import { readFile as readFile12 } from "node:fs/promises";
15688
16656
  async function init(argv) {
15689
16657
  const skipTranscripts = argv.includes("--no-transcripts");
15690
16658
  const flag = (f) => argv.includes(f) ? argv[argv.indexOf(f) + 1] ?? null : null;
@@ -15713,13 +16681,23 @@ async function init(argv) {
15713
16681
  return 1;
15714
16682
  }
15715
16683
  step(2, "Connecting the project Brain\u2026");
16684
+ const known = await captureProject(root);
16685
+ const reuse = known && (!brainFlag || known.brainId === brainFlag) ? known : null;
15716
16686
  const conn = await captureApi("/capture/connect", {
15717
16687
  method: "POST",
15718
- body: { root, name, ...brainFlag ? { brainId: brainFlag } : {} }
16688
+ body: {
16689
+ name,
16690
+ ...brainFlag ? { brainId: brainFlag } : reuse ? { brainId: reuse.brainId } : {},
16691
+ ...reuse ? { sourceId: reuse.sourceId } : {}
16692
+ }
15719
16693
  });
15720
16694
  ok(
15721
16695
  conn.createdBrain ? `created Brain "${name}" (${conn.brainId})` : `reusing Brain "${name}" (${conn.brainId})`
15722
16696
  );
16697
+ writeProjectLink(root, { brainId: conn.brainId, sourceId: conn.sourceId });
16698
+ ok(
16699
+ `folder linked to the Brain in ${projectsPath()} (the path stays on this machine)`
16700
+ );
15723
16701
  if (serverFlag || tokenFlag) {
15724
16702
  writeUserConfig({
15725
16703
  ...serverFlag ? { BRAINJAR_SERVER: serverFlag } : {},
@@ -15731,7 +16709,7 @@ async function init(argv) {
15731
16709
  const graph = await scanStructure(root);
15732
16710
  const structure = await captureApi(
15733
16711
  "/capture/structure",
15734
- { method: "POST", body: { root, graph } }
16712
+ { method: "POST", body: { brain: conn.brainId, graph } }
15735
16713
  );
15736
16714
  const connector = createConnector("git_local", { cwd: root }, {});
15737
16715
  const artifacts = await connector.sync(conn.cursor ?? void 0);
@@ -15743,7 +16721,7 @@ async function init(argv) {
15743
16721
  {
15744
16722
  method: "POST",
15745
16723
  body: {
15746
- root,
16724
+ brain: conn.brainId,
15747
16725
  sourceId: conn.sourceId,
15748
16726
  artifacts,
15749
16727
  cursor: maxUpdatedAt(artifacts) ?? conn.cursor
@@ -15769,11 +16747,11 @@ async function init(argv) {
15769
16747
  let skipped = 0;
15770
16748
  for (const f of files) {
15771
16749
  try {
15772
- const { text, truncated } = capTranscript(await readFile8(f, "utf8"));
16750
+ const { text, truncated } = capTranscript(await readFile12(f, "utf8"));
15773
16751
  if (truncated) truncatedCount++;
15774
16752
  const r = await captureApi("/capture/transcript", {
15775
16753
  method: "POST",
15776
- body: { root, text, author, backfill: true }
16754
+ body: { brain: conn.brainId, text, author, backfill: true }
15777
16755
  });
15778
16756
  prompts += r.prompts;
15779
16757
  failures += r.failures;
@@ -15792,7 +16770,7 @@ async function init(argv) {
15792
16770
  const tools = detectedTools(root);
15793
16771
  if (tools.length === 0) {
15794
16772
  ok(
15795
- "no coding agent detected (looked for Claude Code, Cursor, Codex, GitHub Copilot); rerun init after installing one"
16773
+ "no coding agent detected (looked for Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, Kiro, OpenCode, Cline); rerun init after installing one"
15796
16774
  );
15797
16775
  }
15798
16776
  for (const tool of tools) {
@@ -15816,7 +16794,7 @@ async function init(argv) {
15816
16794
  ok("skipped (server unreachable) \u2014 the sync hook will add it");
15817
16795
  }
15818
16796
  const final = await captureApi(
15819
- `/capture/project?root=${encodeURIComponent(root)}`
16797
+ `/capture/project?brain=${conn.brainId}&source=${conn.sourceId}`
15820
16798
  );
15821
16799
  console.log(
15822
16800
  `
@@ -15859,7 +16837,7 @@ __export(constraints_exports, {
15859
16837
  constraintsMain: () => constraintsMain,
15860
16838
  sessionStartEnvelope: () => sessionStartEnvelope
15861
16839
  });
15862
- import { readFileSync as readFileSync9 } from "node:fs";
16840
+ import { readFileSync as readFileSync13 } from "node:fs";
15863
16841
  function sessionStartEnvelope(text) {
15864
16842
  return JSON.stringify({
15865
16843
  hookSpecificOutput: {
@@ -15872,12 +16850,13 @@ async function constraintsMain(argv) {
15872
16850
  const hook = argv.includes("--hook");
15873
16851
  const brainFlag = argv[argv.indexOf("--brain") + 1];
15874
16852
  const brainOverride = argv.includes("--brain") ? brainFlag ?? null : null;
16853
+ const envelope = argv.includes("--copilot") ? copilotContextEnvelope : argv.includes("--cline") ? clineContextEnvelope : argv.includes("--plain") ? (text) => text : sessionStartEnvelope;
15875
16854
  try {
15876
16855
  let root;
15877
16856
  if (hook) {
15878
16857
  let cwd;
15879
16858
  try {
15880
- const payload = JSON.parse(readFileSync9(0, "utf8"));
16859
+ const payload = JSON.parse(readFileSync13(0, "utf8"));
15881
16860
  cwd = payload.cwd ?? payload.workspace_roots?.[0] ?? payload.workspaceRoots?.[0];
15882
16861
  } catch {
15883
16862
  }
@@ -15885,22 +16864,19 @@ async function constraintsMain(argv) {
15885
16864
  } else {
15886
16865
  root = await repoRoot();
15887
16866
  }
15888
- const q = new URLSearchParams({ root, limit: "5" });
15889
- if (brainOverride) q.set("brain", brainOverride);
15890
- const r = await captureApi(
15891
- `/capture/constraints?${q}`
15892
- );
15893
- if (!r.brainId) {
16867
+ const brainId = brainOverride ?? (await captureProject(root))?.brainId;
16868
+ if (!brainId) {
15894
16869
  if (!hook) {
15895
- console.error(
15896
- `brainjar: no Brain is connected to ${root}. Pass --brain <id> or run \`npx brainjar init\` first.`
15897
- );
16870
+ console.error(notConnected(root));
15898
16871
  process.exitCode = 1;
15899
16872
  }
15900
16873
  return;
15901
16874
  }
15902
- const envelope = argv.includes("--copilot") ? copilotContextEnvelope : sessionStartEnvelope;
16875
+ const r = await captureApi(
16876
+ `/capture/constraints?brain=${encodeURIComponent(brainId)}&limit=5`
16877
+ );
15903
16878
  if (r.text) console.log(hook ? envelope(r.text) : r.text);
16879
+ else if (hook && argv.includes("--cline")) console.log(CLINE_REPLY);
15904
16880
  else if (!hook)
15905
16881
  console.log("No recorded failed approaches or rules for this Brain.");
15906
16882
  if (hook) {
@@ -15911,7 +16887,12 @@ async function constraintsMain(argv) {
15911
16887
  }
15912
16888
  }
15913
16889
  } catch (e) {
15914
- console.error("brainjar constraints:", e);
16890
+ if (hook && e instanceof TokenError) console.log(envelope(e.message));
16891
+ else
16892
+ console.error(
16893
+ "brainjar constraints:",
16894
+ e instanceof Error ? e.message : e
16895
+ );
15915
16896
  if (!hook) process.exitCode = 1;
15916
16897
  }
15917
16898
  }
@@ -15921,6 +16902,7 @@ var init_constraints = __esm({
15921
16902
  init_src3();
15922
16903
  init_api_client();
15923
16904
  init_copilot();
16905
+ init_cline();
15924
16906
  }
15925
16907
  });
15926
16908
 
@@ -15972,22 +16954,27 @@ async function runDoctor(root, opts = {}) {
15972
16954
  `${serverUrl()} unreachable (${String(e).slice(0, 120)})`
15973
16955
  );
15974
16956
  }
16957
+ const link = await captureProject(root);
15975
16958
  if (serverOk) {
15976
16959
  try {
15977
- const project = await captureApi(`/capture/project?root=${encodeURIComponent(root)}`);
16960
+ const project = await captureApi(
16961
+ link ? `/capture/project?brain=${link.brainId}&source=${link.sourceId}` : "/capture/project"
16962
+ );
15978
16963
  add("token", true, "accepted");
15979
16964
  add(
15980
16965
  "brain",
15981
16966
  project.brainId !== null,
15982
- project.brainId !== null ? `${project.brainId} (${project.objects ?? 0} memory objects)` : "no Brain connected to this repo. Run npx brainjar init"
16967
+ project.brainId !== null ? `${project.brainId} (${project.objects ?? 0} memory objects)` : "not connected on this machine. Run npx brainjar init --brain <id>"
15983
16968
  );
15984
16969
  } catch (e) {
15985
- add(
15986
- "token",
15987
- false,
15988
- String(e instanceof Error ? e.message : e).slice(0, 160)
15989
- );
15990
- add("brain", false, "skipped (token rejected)");
16970
+ const msg = String(e instanceof Error ? e.message : e).slice(0, 200);
16971
+ if (e instanceof TokenError) {
16972
+ add("token", false, msg);
16973
+ add("brain", false, "skipped (token rejected)");
16974
+ } else {
16975
+ add("token", true, "accepted");
16976
+ add("brain", false, msg);
16977
+ }
15991
16978
  }
15992
16979
  } else {
15993
16980
  add("token", false, "skipped (server unreachable)");
@@ -16072,8 +17059,8 @@ __export(deepscan_exports, {
16072
17059
  deepscanMain: () => deepscanMain,
16073
17060
  selectDeepScanFiles: () => selectDeepScanFiles
16074
17061
  });
16075
- import { readdir as readdir3, readFile as readFile9, stat as stat2 } from "node:fs/promises";
16076
- import path14 from "node:path";
17062
+ import { readdir as readdir3, readFile as readFile13, stat as stat2 } from "node:fs/promises";
17063
+ import path18 from "node:path";
16077
17064
  function selectDeepScanFiles(candidates, graph, maxFiles = MAX_FILES) {
16078
17065
  const picked = [];
16079
17066
  const has = /* @__PURE__ */ new Set();
@@ -16110,7 +17097,7 @@ async function walkCandidates(root) {
16110
17097
  for (const e of entries) {
16111
17098
  if (!e.isFile()) continue;
16112
17099
  const parent = e.parentPath ?? e.path ?? root;
16113
- const rel = path14.relative(root, path14.join(parent, e.name)).split(path14.sep).join("/");
17100
+ const rel = path18.relative(root, path18.join(parent, e.name)).split(path18.sep).join("/");
16114
17101
  if (rel.split("/").some((seg) => SKIP_DIRS.has(seg))) continue;
16115
17102
  out.push(rel);
16116
17103
  }
@@ -16118,11 +17105,9 @@ async function walkCandidates(root) {
16118
17105
  }
16119
17106
  async function deepscanMain() {
16120
17107
  const root = await repoRoot();
16121
- const project = await captureApi(`/capture/project?root=${encodeURIComponent(root)}`);
16122
- if (!project.brainId || !project.sourceId) {
16123
- console.error(
16124
- `brainjar: no Brain is connected to ${root} \u2014 run \`npx brainjar init\` first`
16125
- );
17108
+ const project = await captureProject(root);
17109
+ if (!project) {
17110
+ console.error(notConnected(root));
16126
17111
  process.exitCode = 1;
16127
17112
  return;
16128
17113
  }
@@ -16135,10 +17120,10 @@ async function deepscanMain() {
16135
17120
  const artifacts = [];
16136
17121
  for (const rel of files) {
16137
17122
  try {
16138
- const abs = path14.join(root, rel);
17123
+ const abs = path18.join(root, rel);
16139
17124
  const info = await stat2(abs);
16140
17125
  if (info.size > RAW_SKIP_BYTES) continue;
16141
- const content = await readFile9(abs, "utf8");
17126
+ const content = await readFile13(abs, "utf8");
16142
17127
  artifacts.push({
16143
17128
  source: "deepscan",
16144
17129
  kind: "source_file",
@@ -16166,7 +17151,7 @@ async function deepscanMain() {
16166
17151
  {
16167
17152
  method: "POST",
16168
17153
  body: {
16169
- root,
17154
+ brain: project.brainId,
16170
17155
  sourceId: project.sourceId,
16171
17156
  artifacts: chunk,
16172
17157
  // Only the final chunk kicks the server-side synthesis: one scan per run.