@dosu/cli 0.53.0-beta.9 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/dosu.js +1147 -639
  3. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -5018,7 +5018,7 @@ function isNpxInvocation(channel = INSTALL_CHANNEL, env = process.env) {
5018
5018
  function getVersionString() {
5019
5019
  return `v${VERSION}`;
5020
5020
  }
5021
- var VERSION = "0.53.0-beta.9", INSTALL_CHANNEL = "npm";
5021
+ var VERSION = "0.53.0", INSTALL_CHANNEL = "npm";
5022
5022
 
5023
5023
  // src/debug/logger.ts
5024
5024
  import {
@@ -5712,12 +5712,34 @@ function answeredEcho(message, answer) {
5712
5712
  function optionLabel(option) {
5713
5713
  return option.label ?? String(option.value);
5714
5714
  }
5715
+ function viewport(count, cursor3, top, rows, chrome) {
5716
+ const visible = Math.min(count, Math.max(4, (rows ?? 24) - chrome));
5717
+ let next = top;
5718
+ if (cursor3 < next)
5719
+ next = cursor3;
5720
+ if (cursor3 >= next + visible)
5721
+ next = cursor3 - visible + 1;
5722
+ next = Math.max(0, Math.min(next, count - visible));
5723
+ return { top: next, visible, above: next, below: count - next - visible };
5724
+ }
5725
+ function moreAbove(n2) {
5726
+ return n2 > 0 ? [import_picocolors11.default.dim(` ↑ ${n2} more`)] : [];
5727
+ }
5728
+ function moreBelow(n2) {
5729
+ return n2 > 0 ? [import_picocolors11.default.dim(` ↓ ${n2} more`)] : [];
5730
+ }
5715
5731
  function select(opts, io = {}) {
5732
+ const { output } = resolveIO(io);
5716
5733
  let selected = 0;
5734
+ let top = 0;
5717
5735
  const labelWidth = Math.max(...opts.options.map((o2) => optionLabel(o2).length));
5718
5736
  return runInteractive({
5719
5737
  render() {
5720
- const rows = opts.options.map((option, index) => {
5738
+ const count = opts.options.length;
5739
+ const view = viewport(count, selected, top, output.rows, 8);
5740
+ top = view.top;
5741
+ const rows = opts.options.slice(top, top + view.visible).map((option, offset) => {
5742
+ const index = top + offset;
5721
5743
  const label = optionLabel(option).padEnd(labelWidth);
5722
5744
  const hint = option.hint ? ` ${import_picocolors11.default.dim(option.hint)}` : "";
5723
5745
  return index === selected ? `${brand(POINTER2)} ${import_picocolors11.default.bold(brand(label))}${hint}` : ` ${import_picocolors11.default.dim(label)}${hint}`;
@@ -5725,7 +5747,9 @@ function select(opts, io = {}) {
5725
5747
  return [
5726
5748
  import_picocolors11.default.bold(opts.message),
5727
5749
  "",
5750
+ ...moreAbove(view.above),
5728
5751
  ...rows,
5752
+ ...moreBelow(view.below),
5729
5753
  "",
5730
5754
  import_picocolors11.default.dim(`↑↓ move ${DOT} enter select ${DOT} esc cancel`)
5731
5755
  ];
@@ -5763,13 +5787,9 @@ function multiselect2(opts, io = {}) {
5763
5787
  return runInteractive({
5764
5788
  render() {
5765
5789
  const count = opts.options.length;
5766
- const visible = Math.min(count, Math.max(4, (output.rows ?? 24) - 9));
5767
- if (cursor3 < top)
5768
- top = cursor3;
5769
- if (cursor3 >= top + visible)
5770
- top = cursor3 - visible + 1;
5771
- top = Math.max(0, Math.min(top, count - visible));
5772
- const rows = opts.options.slice(top, top + visible).map((option, offset) => {
5790
+ const view = viewport(count, cursor3, top, output.rows, 9);
5791
+ top = view.top;
5792
+ const rows = opts.options.slice(top, top + view.visible).map((option, offset) => {
5773
5793
  const index = top + offset;
5774
5794
  const isPicked = picked.has(option.value);
5775
5795
  const box = isPicked ? brand(BOX_ON) : import_picocolors11.default.dim(BOX_OFF);
@@ -5778,15 +5798,13 @@ function multiselect2(opts, io = {}) {
5778
5798
  const suffix = status ? ` ${status}` : "";
5779
5799
  return index === cursor3 ? `${brand(POINTER2)} ${box} ${import_picocolors11.default.bold(brand(label))}${suffix}` : ` ${box} ${import_picocolors11.default.dim(label)}${suffix}`;
5780
5800
  });
5781
- const above = top;
5782
- const below = count - top - visible;
5783
5801
  const summary = opts.summary?.(pickedValues());
5784
5802
  return [
5785
5803
  import_picocolors11.default.bold(opts.message),
5786
5804
  "",
5787
- ...above > 0 ? [import_picocolors11.default.dim(` ↑ ${above} more`)] : [],
5805
+ ...moreAbove(view.above),
5788
5806
  ...rows,
5789
- ...below > 0 ? [import_picocolors11.default.dim(` ↓ ${below} more`)] : [],
5807
+ ...moreBelow(view.below),
5790
5808
  "",
5791
5809
  ...error ? [import_picocolors11.default.yellow(error)] : summary ? [import_picocolors11.default.dim(summary)] : [],
5792
5810
  import_picocolors11.default.dim(`↑↓ move ${DOT} space toggle ${DOT} a all ${DOT} enter confirm ${DOT} esc cancel`)
@@ -7982,6 +8000,14 @@ function mcpURL(deploymentID) {
7982
8000
  function mcpBaseURL() {
7983
8001
  return `${getBackendURL()}/v1/mcp`;
7984
8002
  }
8003
+ function mcpEndpoint(cfg) {
8004
+ if (cfg.mode === MODE_OSS)
8005
+ return mcpBaseURL();
8006
+ const deploymentID = cfg.active_account?.target?.deployment_id;
8007
+ if (!deploymentID)
8008
+ throw new Error("deployment ID is required");
8009
+ return mcpURL(deploymentID);
8010
+ }
7985
8011
  function mcpHeaders(apiKey) {
7986
8012
  if (!apiKey) {
7987
8013
  throw new Error("API key is required. Run 'dosu setup' to create one.");
@@ -8008,18 +8034,23 @@ function mcpRemoteServer(url, apiKey) {
8008
8034
  };
8009
8035
  }
8010
8036
  function loadJSONConfig(path3) {
8037
+ try {
8038
+ return readJSONConfig(path3);
8039
+ } catch {
8040
+ return {};
8041
+ }
8042
+ }
8043
+ function readJSONConfig(path3) {
8011
8044
  if (!existsSync7(path3))
8012
8045
  return {};
8013
- let data = readFileSync7(path3, "utf-8").trim();
8014
- if (!data)
8046
+ const raw = readFileSync7(path3, "utf-8").trim();
8047
+ if (!raw)
8015
8048
  return {};
8016
- if (path3.endsWith(".jsonc")) {
8017
- data = stripJSONComments(data);
8018
- }
8019
8049
  try {
8020
- return JSON.parse(data);
8021
- } catch {
8022
- return {};
8050
+ return JSON.parse(stripTrailingCommas(stripJSONComments(raw)));
8051
+ } catch (err) {
8052
+ const detail = err.message;
8053
+ throw new Error(`Could not parse ${path3} as JSON (${detail}). Fix the file and retry.`);
8023
8054
  }
8024
8055
  }
8025
8056
  function stripJSONComments(data) {
@@ -8027,25 +8058,7 @@ function stripJSONComments(data) {
8027
8058
  let i2 = 0;
8028
8059
  while (i2 < data.length) {
8029
8060
  if (data[i2] === '"') {
8030
- result.push(data[i2]);
8031
- i2++;
8032
- while (i2 < data.length && data[i2] !== '"') {
8033
- if (data[i2] === "\\") {
8034
- result.push(data[i2]);
8035
- i2++;
8036
- if (i2 < data.length) {
8037
- result.push(data[i2]);
8038
- i2++;
8039
- }
8040
- continue;
8041
- }
8042
- result.push(data[i2]);
8043
- i2++;
8044
- }
8045
- if (i2 < data.length) {
8046
- result.push(data[i2]);
8047
- i2++;
8048
- }
8061
+ i2 = copyStringLiteral(data, i2, result);
8049
8062
  continue;
8050
8063
  }
8051
8064
  if (i2 + 1 < data.length && data[i2] === "/" && data[i2 + 1] === "/") {
@@ -8059,10 +8072,46 @@ function stripJSONComments(data) {
8059
8072
  i2 += 2;
8060
8073
  while (i2 + 1 < data.length && !(data[i2] === "*" && data[i2 + 1] === "/"))
8061
8074
  i2++;
8062
- if (i2 + 1 < data.length)
8063
- i2 += 2;
8075
+ i2 = i2 + 1 < data.length ? i2 + 2 : data.length;
8076
+ continue;
8077
+ }
8078
+ result.push(data[i2]);
8079
+ i2++;
8080
+ }
8081
+ return result.join("");
8082
+ }
8083
+ function copyStringLiteral(data, start, out) {
8084
+ let i2 = start + 1;
8085
+ out.push('"');
8086
+ while (i2 < data.length && data[i2] !== '"') {
8087
+ out.push(data[i2]);
8088
+ if (data[i2] === "\\" && i2 + 1 < data.length)
8089
+ out.push(data[++i2]);
8090
+ i2++;
8091
+ }
8092
+ if (i2 < data.length) {
8093
+ out.push('"');
8094
+ i2++;
8095
+ }
8096
+ return i2;
8097
+ }
8098
+ function stripTrailingCommas(data) {
8099
+ const result = [];
8100
+ let i2 = 0;
8101
+ while (i2 < data.length) {
8102
+ if (data[i2] === '"') {
8103
+ i2 = copyStringLiteral(data, i2, result);
8064
8104
  continue;
8065
8105
  }
8106
+ if (data[i2] === ",") {
8107
+ let j = i2 + 1;
8108
+ while (j < data.length && /\s/.test(data[j]))
8109
+ j++;
8110
+ if (data[j] === "}" || data[j] === "]") {
8111
+ i2++;
8112
+ continue;
8113
+ }
8114
+ }
8066
8115
  result.push(data[i2]);
8067
8116
  i2++;
8068
8117
  }
@@ -8086,7 +8135,7 @@ function isJSONKeyConfigured(configPath, topLevelKey) {
8086
8135
  return "dosu" in section;
8087
8136
  }
8088
8137
  function installJSONServer(configPath, topKey, server) {
8089
- const jsonCfg = loadJSONConfig(configPath);
8138
+ const jsonCfg = readJSONConfig(configPath);
8090
8139
  let section = jsonCfg[topKey];
8091
8140
  if (typeof section !== "object" || section === null) {
8092
8141
  section = {};
@@ -8098,18 +8147,19 @@ function installJSONServer(configPath, topKey, server) {
8098
8147
  function removeJSONServer(configPath, topKey) {
8099
8148
  let jsonCfg;
8100
8149
  try {
8101
- jsonCfg = loadJSONConfig(configPath);
8150
+ jsonCfg = readJSONConfig(configPath);
8102
8151
  } catch {
8103
8152
  return;
8104
8153
  }
8105
8154
  const section = jsonCfg[topKey];
8106
- if (typeof section === "object" && section !== null) {
8107
- delete section.dosu;
8108
- }
8155
+ if (typeof section !== "object" || section === null || !("dosu" in section))
8156
+ return;
8157
+ delete section.dosu;
8109
8158
  saveJSONConfig(configPath, jsonCfg);
8110
8159
  }
8111
8160
  var import_write_file_atomic, writeFileAtomic, MCP_REMOTE_VERSION = "0.1.38";
8112
8161
  var init_config_helpers = __esm(() => {
8162
+ init_config();
8113
8163
  import_write_file_atomic = __toESM(require_lib(), 1);
8114
8164
  writeFileAtomic = import_write_file_atomic.default;
8115
8165
  });
@@ -8358,16 +8408,164 @@ var init_agents = __esm(() => {
8358
8408
  init_formats();
8359
8409
  });
8360
8410
 
8411
+ // src/sessions/project-dir.ts
8412
+ import {
8413
+ closeSync as closeSync2,
8414
+ existsSync as existsSync9,
8415
+ mkdirSync as mkdirSync6,
8416
+ openSync as openSync2,
8417
+ readFileSync as readFileSync9,
8418
+ readSync,
8419
+ renameSync as renameSync2,
8420
+ statSync as statSync3,
8421
+ writeFileSync as writeFileSync6
8422
+ } from "node:fs";
8423
+ import { join as join8 } from "node:path";
8424
+ function cachePath(configDir) {
8425
+ return join8(configDir, CACHE_FILENAME2);
8426
+ }
8427
+ function loadCacheFile(configDir) {
8428
+ try {
8429
+ const raw = JSON.parse(readFileSync9(cachePath(configDir), "utf-8"));
8430
+ if (raw.schema_version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== "object")
8431
+ return {};
8432
+ return raw.entries;
8433
+ } catch {
8434
+ return {};
8435
+ }
8436
+ }
8437
+ function saveCacheFile(configDir, entries) {
8438
+ try {
8439
+ if (!existsSync9(configDir))
8440
+ mkdirSync6(configDir, { recursive: true, mode: 448 });
8441
+ const path3 = cachePath(configDir);
8442
+ const tmp = `${path3}.${process.pid}.tmp`;
8443
+ const file = { schema_version: CACHE_SCHEMA_VERSION, entries };
8444
+ writeFileSync6(tmp, JSON.stringify(file), { mode: 384 });
8445
+ renameSync2(tmp, path3);
8446
+ } catch {}
8447
+ }
8448
+ function readHead(path3, bytes = HEAD_BYTES) {
8449
+ let fd;
8450
+ try {
8451
+ fd = openSync2(path3, "r");
8452
+ } catch {
8453
+ return null;
8454
+ }
8455
+ try {
8456
+ const buf = Buffer.alloc(bytes);
8457
+ const read = readSync(fd, buf, 0, bytes, 0);
8458
+ return buf.subarray(0, read).toString("utf-8");
8459
+ } catch {
8460
+ return null;
8461
+ } finally {
8462
+ closeSync2(fd);
8463
+ }
8464
+ }
8465
+ function cwdFromJsonlHead(text) {
8466
+ for (const line of text.split(`
8467
+ `).slice(0, HEAD_LINES)) {
8468
+ if (!line.includes('"cwd"'))
8469
+ continue;
8470
+ try {
8471
+ const obj = JSON.parse(line);
8472
+ const cwd = obj.cwd ?? obj.payload?.cwd;
8473
+ if (typeof cwd === "string" && cwd.startsWith("/"))
8474
+ return cwd;
8475
+ } catch {}
8476
+ }
8477
+ return null;
8478
+ }
8479
+ function unmungeSlug(slug, exists = existsSync9) {
8480
+ const tokens = slug.replace(/^-/, "").split("-");
8481
+ if (tokens.length === 0 || tokens[0] === "")
8482
+ return null;
8483
+ let budget = 5000;
8484
+ const walk = (prefix, index) => {
8485
+ if (budget-- <= 0)
8486
+ return null;
8487
+ if (index === tokens.length)
8488
+ return exists(prefix) ? prefix : null;
8489
+ const asSegment = prefix === "" || exists(prefix) ? walk(`${prefix}/${tokens[index]}`, index + 1) : null;
8490
+ if (asSegment)
8491
+ return asSegment;
8492
+ return prefix === "" ? null : walk(`${prefix}-${tokens[index]}`, index + 1);
8493
+ };
8494
+ return walk("", 0);
8495
+ }
8496
+ function fileMtime(path3) {
8497
+ try {
8498
+ return statSync3(path3).mtime.toISOString();
8499
+ } catch {
8500
+ return "";
8501
+ }
8502
+ }
8503
+ function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
8504
+ const exists = deps.exists ?? existsSync9;
8505
+ const head = deps.readHead ?? readHead;
8506
+ const mtime = deps.mtime ?? fileMtime;
8507
+ const entries = loadCacheFile(configDir);
8508
+ let dirty = false;
8509
+ const compute = (session) => {
8510
+ switch (session.harness) {
8511
+ case "opencode":
8512
+ return session.project ?? null;
8513
+ case "claude": {
8514
+ const text = head(session.path);
8515
+ const cwd = text ? cwdFromJsonlHead(text) : null;
8516
+ if (cwd)
8517
+ return cwd;
8518
+ return session.project ? unmungeSlug(session.project, exists) : null;
8519
+ }
8520
+ case "codex": {
8521
+ const text = head(session.path);
8522
+ return text ? cwdFromJsonlHead(text) : null;
8523
+ }
8524
+ case "cursor":
8525
+ return session.project ? unmungeSlug(session.project, exists) : null;
8526
+ default:
8527
+ return null;
8528
+ }
8529
+ };
8530
+ return {
8531
+ cached(key) {
8532
+ return entries[key]?.dir ?? null;
8533
+ },
8534
+ resolve(session) {
8535
+ const key = `${session.harness}/${session.id}`;
8536
+ const cached = entries[key];
8537
+ if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
8538
+ return cached.dir;
8539
+ }
8540
+ const dir = compute(session);
8541
+ entries[key] = { dir, mtime: mtime(session.path) };
8542
+ dirty = true;
8543
+ return dir;
8544
+ },
8545
+ flush() {
8546
+ if (!dirty)
8547
+ return;
8548
+ saveCacheFile(configDir, entries);
8549
+ dirty = false;
8550
+ }
8551
+ };
8552
+ }
8553
+ var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
8554
+ var init_project_dir = __esm(() => {
8555
+ init_config();
8556
+ HEAD_BYTES = 128 * 1024;
8557
+ });
8558
+
8361
8559
  // src/sessions/scan.ts
8362
- import { existsSync as existsSync9, readdirSync, statSync as statSync3 } from "node:fs";
8560
+ import { existsSync as existsSync10, readdirSync, statSync as statSync4 } from "node:fs";
8363
8561
  import { createRequire as createRequire2 } from "node:module";
8364
8562
  import { homedir as homedir3 } from "node:os";
8365
- import { basename, join as join8 } from "node:path";
8563
+ import { basename, join as join9 } from "node:path";
8366
8564
  function listDir(dir) {
8367
8565
  try {
8368
8566
  return readdirSync(dir, { withFileTypes: true }).map((entry) => ({
8369
8567
  name: entry.name,
8370
- path: join8(dir, entry.name),
8568
+ path: join9(dir, entry.name),
8371
8569
  isDir: entry.isDirectory()
8372
8570
  }));
8373
8571
  } catch {
@@ -8380,7 +8578,7 @@ function sessionFromFile(filePath, harness, project) {
8380
8578
  return null;
8381
8579
  let mtime;
8382
8580
  try {
8383
- mtime = statSync3(filePath).mtime;
8581
+ mtime = statSync4(filePath).mtime;
8384
8582
  } catch {
8385
8583
  return null;
8386
8584
  }
@@ -8394,7 +8592,7 @@ function sessionFromFile(filePath, harness, project) {
8394
8592
  }
8395
8593
  function scanClaude(home) {
8396
8594
  const sessions = [];
8397
- for (const project of listDir(join8(home, ".claude", "projects"))) {
8595
+ for (const project of listDir(join9(home, ".claude", "projects"))) {
8398
8596
  if (!project.isDir)
8399
8597
  continue;
8400
8598
  for (const entry of listDir(project.path)) {
@@ -8409,10 +8607,10 @@ function scanClaude(home) {
8409
8607
  }
8410
8608
  function scanCursor(home) {
8411
8609
  const sessions = [];
8412
- for (const project of listDir(join8(home, ".cursor", "projects"))) {
8610
+ for (const project of listDir(join9(home, ".cursor", "projects"))) {
8413
8611
  if (!project.isDir)
8414
8612
  continue;
8415
- for (const entry of listDir(join8(project.path, "agent-transcripts"))) {
8613
+ for (const entry of listDir(join9(project.path, "agent-transcripts"))) {
8416
8614
  const files = entry.isDir ? listDir(entry.path).filter((f2) => !f2.isDir) : [entry];
8417
8615
  for (const file of files) {
8418
8616
  const session = sessionFromFile(file.path, "cursor", project.name);
@@ -8424,9 +8622,9 @@ function scanCursor(home) {
8424
8622
  return sessions;
8425
8623
  }
8426
8624
  function scanCodex(home, env) {
8427
- const codexHome2 = env.CODEX_HOME ?? join8(home, ".codex");
8625
+ const codexHome2 = env.CODEX_HOME ?? join9(home, ".codex");
8428
8626
  const sessions = [];
8429
- for (const year of listDir(join8(codexHome2, "sessions"))) {
8627
+ for (const year of listDir(join9(codexHome2, "sessions"))) {
8430
8628
  if (!year.isDir)
8431
8629
  continue;
8432
8630
  for (const month of listDir(year.path)) {
@@ -8482,9 +8680,9 @@ function querySqlite(dbPath, sql) {
8482
8680
  }
8483
8681
  }
8484
8682
  function scanOpencode(home, env) {
8485
- const dataDir = env.XDG_DATA_HOME ?? join8(home, ".local", "share");
8486
- const dbPath = join8(dataDir, "opencode", "opencode.db");
8487
- if (!existsSync9(dbPath))
8683
+ const dataDir = env.XDG_DATA_HOME ?? join9(home, ".local", "share");
8684
+ const dbPath = join9(dataDir, "opencode", "opencode.db");
8685
+ if (!existsSync10(dbPath))
8488
8686
  return [];
8489
8687
  const rows = querySqlite(dbPath, "SELECT id, directory, time_updated FROM session WHERE parent_id IS NULL");
8490
8688
  if (!rows)
@@ -8522,10 +8720,10 @@ function scanAgentSessions(options = {}) {
8522
8720
  var init_scan = () => {};
8523
8721
 
8524
8722
  // src/sync/watermark.ts
8525
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
8526
- import { join as join9 } from "node:path";
8723
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "node:fs";
8724
+ import { join as join10 } from "node:path";
8527
8725
  function syncStatePath(configDir = getConfigDir()) {
8528
- return join9(configDir, STATE_FILENAME);
8726
+ return join10(configDir, STATE_FILENAME);
8529
8727
  }
8530
8728
  function loadSyncState(configDir = getConfigDir()) {
8531
8729
  const empty = {
@@ -8534,13 +8732,13 @@ function loadSyncState(configDir = getConfigDir()) {
8534
8732
  consecutive_failures: 0
8535
8733
  };
8536
8734
  const path3 = syncStatePath(configDir);
8537
- if (!existsSync10(path3))
8735
+ if (!existsSync11(path3))
8538
8736
  return empty;
8539
8737
  try {
8540
- const raw = JSON.parse(readFileSync9(path3, "utf-8"));
8738
+ const raw = JSON.parse(readFileSync10(path3, "utf-8"));
8541
8739
  if (raw.schema_version !== STATE_SCHEMA_VERSION)
8542
8740
  return empty;
8543
- const minedSessions = Array.isArray(raw.mined_sessions) ? raw.mined_sessions.filter((record) => typeof record === "object" && record !== null && typeof record.at === "string" && typeof record.session === "string").map((record) => ({
8741
+ const studiedSessions = Array.isArray(raw.mined_sessions) ? raw.mined_sessions.filter((record) => typeof record === "object" && record !== null && typeof record.at === "string" && typeof record.session === "string").map((record) => ({
8544
8742
  at: record.at,
8545
8743
  session: record.session,
8546
8744
  ...typeof record.project === "string" ? { project: record.project } : {}
@@ -8554,8 +8752,8 @@ function loadSyncState(configDir = getConfigDir()) {
8554
8752
  watermark: typeof raw.watermark === "string" ? raw.watermark : null,
8555
8753
  last_attempt_at: typeof raw.last_attempt_at === "string" ? raw.last_attempt_at : undefined,
8556
8754
  consecutive_failures: typeof raw.consecutive_failures === "number" && raw.consecutive_failures >= 0 ? raw.consecutive_failures : 0,
8557
- mined_sessions: minedSessions,
8558
- total_mined: typeof raw.total_mined === "number" && raw.total_mined >= 0 ? raw.total_mined : minedSessions.length,
8755
+ mined_sessions: studiedSessions,
8756
+ total_mined: typeof raw.total_mined === "number" && raw.total_mined >= 0 ? raw.total_mined : studiedSessions.length,
8559
8757
  total_notes: typeof raw.total_notes === "number" && raw.total_notes >= 0 ? raw.total_notes : 0,
8560
8758
  total_learning_tokens: typeof raw.total_learning_tokens === "number" && raw.total_learning_tokens >= 0 ? raw.total_learning_tokens : 0,
8561
8759
  ...lastRefusal ? { last_refusal: lastRefusal } : {},
@@ -8577,13 +8775,24 @@ function setSyncPaused(paused, configDir = getConfigDir()) {
8577
8775
  delete state.paused;
8578
8776
  saveSyncState(state, configDir);
8579
8777
  }
8778
+ function resetSyncState(configDir = getConfigDir()) {
8779
+ const previous = loadSyncState(configDir);
8780
+ const fresh = {
8781
+ schema_version: STATE_SCHEMA_VERSION,
8782
+ watermark: null,
8783
+ consecutive_failures: 0,
8784
+ ...previous.project_filter ? { project_filter: previous.project_filter } : {},
8785
+ ...previous.paused ? { paused: true } : {}
8786
+ };
8787
+ saveSyncState(fresh, configDir);
8788
+ }
8580
8789
  function saveSyncState(state, configDir = getConfigDir()) {
8581
- if (!existsSync10(configDir))
8582
- mkdirSync6(configDir, { recursive: true, mode: 448 });
8790
+ if (!existsSync11(configDir))
8791
+ mkdirSync7(configDir, { recursive: true, mode: 448 });
8583
8792
  const path3 = syncStatePath(configDir);
8584
8793
  const tmp = `${path3}.${process.pid}.tmp`;
8585
- writeFileSync6(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8586
- renameSync2(tmp, path3);
8794
+ writeFileSync7(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8795
+ renameSync3(tmp, path3);
8587
8796
  }
8588
8797
  function backoffUntil(state) {
8589
8798
  if (state.consecutive_failures === 0 || !state.last_attempt_at)
@@ -8627,7 +8836,7 @@ function gateSessions(sessions, watermark, now = new Date, quietPeriodMs = DEFAU
8627
8836
  }
8628
8837
  return { ready, open: open2 };
8629
8838
  }
8630
- var STATE_FILENAME = "knowledge-sync.json", STATE_SCHEMA_VERSION = 1, DEFAULT_QUIET_PERIOD_MS, BACKOFF_BASE_MS, BACKOFF_MAX_MS, MINED_HISTORY_LIMIT = 500, UNKNOWN_PROJECT = "(unknown)";
8839
+ var STATE_FILENAME = "knowledge-sync.json", STATE_SCHEMA_VERSION = 1, DEFAULT_QUIET_PERIOD_MS, BACKOFF_BASE_MS, BACKOFF_MAX_MS, STUDIED_HISTORY_LIMIT = 500, UNKNOWN_PROJECT = "(unknown)";
8631
8840
  var init_watermark = __esm(() => {
8632
8841
  init_config();
8633
8842
  DEFAULT_QUIET_PERIOD_MS = 5 * 60 * 1000;
@@ -8643,20 +8852,34 @@ async function fetchReportNotes(cfg) {
8643
8852
  }
8644
8853
  const { createTypedClient: createTypedClient2 } = await Promise.resolve().then(() => (init_trpc(), exports_trpc));
8645
8854
  const client = createTypedClient2(cfg);
8646
- const { notes } = await client.notes.listMine.query({
8647
- org_id: orgId,
8648
- limit: REPORT_NOTES_LIMIT
8649
- });
8650
- return notes.map((note) => ({
8651
- title: note.title,
8652
- content: note.body,
8653
- at: note.created_at,
8654
- ...note.transcript_id ? { transcript_id: note.transcript_id } : {},
8655
- ...note.repo ? { repo: note.repo } : {},
8656
- ...note.branch ? { branch: note.branch } : {}
8657
- }));
8855
+ const notes = [];
8856
+ let cursor3;
8857
+ let truncated = false;
8858
+ for (let page = 0;page < REPORT_NOTES_MAX_PAGES; page++) {
8859
+ const result = await client.notes.listMine.query({
8860
+ org_id: orgId,
8861
+ limit: REPORT_NOTES_LIMIT,
8862
+ ...cursor3 ? { cursor: cursor3 } : {}
8863
+ });
8864
+ notes.push(...result.notes.map((note) => ({
8865
+ title: note.title,
8866
+ content: note.body,
8867
+ at: note.created_at,
8868
+ ...note.transcript_id ? { transcript_id: note.transcript_id } : {},
8869
+ ...note.repo ? { repo: note.repo } : {},
8870
+ ...note.branch ? { branch: note.branch } : {}
8871
+ })));
8872
+ cursor3 = result.next_cursor;
8873
+ if (!cursor3) {
8874
+ truncated = result.notes.length === REPORT_NOTES_LIMIT;
8875
+ break;
8876
+ }
8877
+ if (page === REPORT_NOTES_MAX_PAGES - 1)
8878
+ truncated = true;
8879
+ }
8880
+ return { notes, truncated };
8658
8881
  }
8659
- var REPORT_NOTES_LIMIT = 500;
8882
+ var REPORT_NOTES_LIMIT = 500, REPORT_NOTES_MAX_PAGES = 40;
8660
8883
 
8661
8884
  // src/report/css.ts
8662
8885
  var REPORT_CSS = `
@@ -8831,38 +9054,6 @@ function applyStatusDefaults(candidates, dryRun) {
8831
9054
  return { ...c2, status: known.has(current) ? current : fallback };
8832
9055
  });
8833
9056
  }
8834
- function tokenTotalsFromCandidates(candidates, inventory) {
8835
- if (candidates.length === 0)
8836
- return null;
8837
- let replaced = 0;
8838
- let hasRediscovery = false;
8839
- let readCost = 0;
8840
- for (const c2 of candidates) {
8841
- const raw = c2.approx_rediscovery_tokens;
8842
- if (raw != null) {
8843
- hasRediscovery = true;
8844
- replaced += Math.max(0, Number(raw) || 0);
8845
- }
8846
- const blob = `${c2.title ?? ""}
8847
- ${c2.content ?? ""}`;
8848
- readCost += Math.round(blob.length / CHARS_PER_TOKEN);
8849
- }
8850
- if (!hasRediscovery)
8851
- return null;
8852
- let baseline = inventory.totals?.learning_tokens ?? 0;
8853
- if (!baseline) {
8854
- baseline = inventory.transcripts.reduce((sum, t2) => sum + (t2.learning_tokens || 0), 0);
8855
- }
8856
- const saved = Math.max(0, replaced - readCost);
8857
- const pct = baseline ? Math.round(1000 * saved / baseline) / 10 : 0;
8858
- return {
8859
- baseline_tokens: baseline,
8860
- replaced_baseline_tokens: replaced,
8861
- read_knowledge_tokens: readCost,
8862
- tokens_saved: saved,
8863
- pct_saved: pct
8864
- };
8865
- }
8866
9057
  function presentationCopy(c2) {
8867
9058
  const idea = (c2.plain_english || "").trim() || (c2.content || "").trim();
8868
9059
  const how = (c2.how_found || "").trim();
@@ -9236,11 +9427,9 @@ function buildReportHtml(options) {
9236
9427
  return bv - av;
9237
9428
  });
9238
9429
  const org = options.orgName || "Your team";
9239
- const repo = options.repo || inventory.cwd || "—";
9240
- const branch = options.branch || "—";
9241
- const summary = options.summary || "Local agent session logs were mined into Dosu notes so the next task can reuse them — reducing rediscovery cost.";
9430
+ const projects = options.projects ?? [];
9431
+ const summary = options.summary || "Local agent session logs were studied and distilled into Dosu notes so the next task can reuse them — reducing rediscovery cost.";
9242
9432
  const generated = `${(options.generatedAt ?? new Date).toISOString().replace("T", " ").slice(0, 16)} UTC`;
9243
- const derived = tokenTotalsFromCandidates(candidates, inventory);
9244
9433
  const [notesHeading, notesLede, footerTitle, footerBody] = notesSectionCopy(candidates);
9245
9434
  const candidateRows = candidates.map((c2, i2) => {
9246
9435
  const [idea, work] = presentationCopy(c2);
@@ -9280,27 +9469,19 @@ function buildReportHtml(options) {
9280
9469
  <td>${esc(query)}</td>
9281
9470
  </tr>`;
9282
9471
  }).join("");
9283
- let tokenSection;
9284
- if (derived) {
9285
- tokenSection = `
9472
+ const scannedTokens = inventory.totals?.learning_tokens || transcripts.reduce((sum, t2) => sum + (t2.learning_tokens || 0), 0);
9473
+ const writtenCount = candidates.filter((c2) => (c2.status || "written").toLowerCase() === "written").length;
9474
+ const writtenLabel = options.dryRun ? "Notes proposed (dry run)" : "Notes written to Dosu";
9475
+ const writtenValue = options.dryRun ? candidates.length : writtenCount;
9476
+ const tokenSection = `
9286
9477
  <section>
9287
- <h2>Estimated context savings</h2>
9288
- <p class="lede">Counterfactual: replace rediscovery stretches with a Dosu <code>read_knowledge</code> hit.</p>
9478
+ <h2>This run</h2>
9289
9479
  <div class="stats">
9290
- <div class="stat"><div class="label">Baseline (cost to learn)</div><div class="value">${fmtInt(derived.baseline_tokens)}</div></div>
9291
- <div class="stat"><div class="label">Learning replaced</div><div class="value">${fmtInt(derived.replaced_baseline_tokens)}</div></div>
9292
- <div class="stat"><div class="label">Read cost</div><div class="value">${fmtInt(derived.read_knowledge_tokens)}</div></div>
9293
- <div class="stat highlight"><div class="label">Est. tokens saved</div><div class="value">${fmtInt(derived.tokens_saved)} <span class="pct">(${esc(`${derived.pct_saved}%`)})</span></div></div>
9480
+ <div class="stat"><div class="label">Tokens scanned</div><div class="value">${fmtInt(scannedTokens)}</div></div>
9481
+ <div class="stat highlight"><div class="label">${esc(writtenLabel)}</div><div class="value">${fmtInt(writtenValue)}</div></div>
9294
9482
  </div>
9295
9483
  </section>`;
9296
- } else {
9297
- tokenSection = `
9298
- <section>
9299
- <h2>Estimated context savings</h2>
9300
- <p class="muted">No rediscovery estimates on the notes yet — each candidate needs <code>approx_rediscovery_tokens</code>.</p>
9301
- </section>`;
9302
- }
9303
- const notesBody = candidateRows.join("") || '<p class="muted">No write_knowledge payloads yet. Run knowledge sync so the miner extracts learnings.</p>';
9484
+ const notesBody = candidateRows.join("") || '<p class="muted">No write_knowledge payloads yet. Run knowledge sync so Dosu studies your sessions and extracts learnings.</p>';
9304
9485
  const lede = notesLede ? `<p class="lede">${esc(notesLede)}</p>` : "";
9305
9486
  return `<!DOCTYPE html>
9306
9487
  <html lang="en">
@@ -9319,9 +9500,8 @@ ${REPORT_CSS}
9319
9500
  <h1>${esc(org)}</h1>
9320
9501
  <p class="lede">${esc(summary)}</p>
9321
9502
  <p class="meta-line">
9322
- Repo <code>${esc(repo)}</code>
9323
- · branch <code>${esc(branch)}</code>
9324
- · generated ${esc(generated)}
9503
+ ${projects.length > 0 ? `Projects ${projects.map((p2) => `<code>${esc(p2)}</code>`).join(", ")} · ` : ""}${transcripts.length} session${transcripts.length === 1 ? "" : "s"} with notes
9504
+ · generated ${esc(generated)}${options.truncated ? " · newest notes only (server capped the fetch)" : ""}
9325
9505
  </p>
9326
9506
  <div class="toolbar no-print">
9327
9507
  <button type="button" onclick="window.print()">Print / Save as PDF</button>
@@ -9367,7 +9547,7 @@ ${REPORT_CSS}
9367
9547
  </html>
9368
9548
  `;
9369
9549
  }
9370
- var CHARS_PER_TOKEN = 4, MAX_TRACE_STEPS = 50, PREVIEW_CHARS = 80, SQL_DISPLAY, LOGS_DISPLAY, SKIP_TOOL_NAMES, TAG_RE, WS_RE;
9550
+ var MAX_TRACE_STEPS = 50, PREVIEW_CHARS = 80, SQL_DISPLAY, LOGS_DISPLAY, SKIP_TOOL_NAMES, TAG_RE, WS_RE;
9371
9551
  var init_html = __esm(() => {
9372
9552
  SQL_DISPLAY = new Set(["execute_sql", "query_run", "list_tables"]);
9373
9553
  LOGS_DISPLAY = new Set(["query_logs"]);
@@ -9377,7 +9557,7 @@ var init_html = __esm(() => {
9377
9557
  });
9378
9558
 
9379
9559
  // src/sessions/read.ts
9380
- import { readFileSync as readFileSync10 } from "node:fs";
9560
+ import { readFileSync as readFileSync11 } from "node:fs";
9381
9561
  function asRecord(value) {
9382
9562
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9383
9563
  }
@@ -9493,11 +9673,11 @@ function readSessionTurns(session) {
9493
9673
  try {
9494
9674
  switch (session.harness) {
9495
9675
  case "claude":
9496
- return readClaude(readFileSync10(session.path, "utf8"));
9676
+ return readClaude(readFileSync11(session.path, "utf8"));
9497
9677
  case "cursor":
9498
- return readCursor(readFileSync10(session.path, "utf8"));
9678
+ return readCursor(readFileSync11(session.path, "utf8"));
9499
9679
  case "codex":
9500
- return readCodex(readFileSync10(session.path, "utf8"));
9680
+ return readCodex(readFileSync11(session.path, "utf8"));
9501
9681
  case "opencode":
9502
9682
  return readOpencode(session.path, session.id);
9503
9683
  }
@@ -9525,7 +9705,7 @@ function countRediscoveryToolCalls(session) {
9525
9705
  case "claude":
9526
9706
  case "cursor": {
9527
9707
  let n2 = 0;
9528
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9708
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9529
9709
  const message = asRecord(record.message);
9530
9710
  for (const name of toolNamesFromContent(message?.content)) {
9531
9711
  if (isRediscoveryTool(name))
@@ -9536,7 +9716,7 @@ function countRediscoveryToolCalls(session) {
9536
9716
  }
9537
9717
  case "codex": {
9538
9718
  let n2 = 0;
9539
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9719
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9540
9720
  const payload = asRecord(record.payload);
9541
9721
  if (payload?.type === "function_call" && typeof payload.name === "string") {
9542
9722
  if (isRediscoveryTool(payload.name))
@@ -9556,9 +9736,9 @@ function estimateSessionTokens(session) {
9556
9736
  let chars = 0;
9557
9737
  for (const turn of readSessionTurns(session))
9558
9738
  chars += turn.text.length;
9559
- return Math.round(chars / CHARS_PER_TOKEN2);
9739
+ return Math.round(chars / CHARS_PER_TOKEN);
9560
9740
  }
9561
- function isWorthMining(session) {
9741
+ function isWorthStudying(session) {
9562
9742
  const turns = readSessionTurns(session);
9563
9743
  if (turns.length < MIN_WORTH_TURNS)
9564
9744
  return false;
@@ -9570,7 +9750,7 @@ function isWorthMining(session) {
9570
9750
  }
9571
9751
  return false;
9572
9752
  }
9573
- var PLAIN_TEXT, CODEX_TEXT, REDISCOVERY_TOOLS, CHARS_PER_TOKEN2 = 4, MIN_WORTH_TURNS = 4, MIN_WORTH_CHARS = 2000;
9753
+ var PLAIN_TEXT, CODEX_TEXT, REDISCOVERY_TOOLS, CHARS_PER_TOKEN = 4, MIN_WORTH_TURNS = 4, MIN_WORTH_CHARS = 2000;
9574
9754
  var init_read = __esm(() => {
9575
9755
  init_scan();
9576
9756
  PLAIN_TEXT = new Set(["text"]);
@@ -9627,12 +9807,12 @@ var init_queries = __esm(() => {
9627
9807
  });
9628
9808
 
9629
9809
  // src/report/digest.ts
9630
- import { readFileSync as readFileSync11 } from "node:fs";
9810
+ import { readFileSync as readFileSync12 } from "node:fs";
9631
9811
  function asRecord2(value) {
9632
9812
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9633
9813
  }
9634
9814
  function estimateTokens(text) {
9635
- return Math.round(text.length / CHARS_PER_TOKEN3);
9815
+ return Math.round(text.length / CHARS_PER_TOKEN2);
9636
9816
  }
9637
9817
  function digestEstTokens(texts, tools, resultChars) {
9638
9818
  const countable = tools.filter((t2) => t2.name !== "tool_result");
@@ -9642,7 +9822,7 @@ function digestEstTokens(texts, tools, resultChars) {
9642
9822
  payload += JSON.stringify(countable);
9643
9823
  let tokens = estimateTokens(payload);
9644
9824
  if (resultChars > 0)
9645
- tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN3));
9825
+ tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN2));
9646
9826
  return tokens;
9647
9827
  }
9648
9828
  function copyToolInputFields(entry, inp) {
@@ -9858,16 +10038,16 @@ function sessionToDigest(session) {
9858
10038
  try {
9859
10039
  switch (session.harness) {
9860
10040
  case "claude":
9861
- return { turns: digestClaude(readFileSync11(session.path, "utf8")) };
10041
+ return { turns: digestClaude(readFileSync12(session.path, "utf8")) };
9862
10042
  case "cursor":
9863
- return { turns: digestCursor(readFileSync11(session.path, "utf8")) };
10043
+ return { turns: digestCursor(readFileSync12(session.path, "utf8")) };
9864
10044
  case "codex":
9865
- return { turns: digestCodex(readFileSync11(session.path, "utf8")) };
10045
+ return { turns: digestCodex(readFileSync12(session.path, "utf8")) };
9866
10046
  case "opencode": {
9867
10047
  const turns = readSessionTurns(session).map((turn, index) => ({
9868
10048
  role: turn.role,
9869
10049
  line: index + 1,
9870
- est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN3),
10050
+ est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN2),
9871
10051
  text: [turn.text],
9872
10052
  tools: []
9873
10053
  }));
@@ -9892,7 +10072,7 @@ function digestTurnText(turn) {
9892
10072
  return texts.filter(Boolean).join(`
9893
10073
  `);
9894
10074
  }
9895
- var CHARS_PER_TOKEN3 = 4, MAX_ASSISTANT_CHARS = 4000, MCP_DIGEST_TOOLS;
10075
+ var CHARS_PER_TOKEN2 = 4, MAX_ASSISTANT_CHARS = 4000, MCP_DIGEST_TOOLS;
9896
10076
  var init_digest = __esm(() => {
9897
10077
  init_read();
9898
10078
  init_queries();
@@ -10083,17 +10263,17 @@ var init_notes = __esm(() => {
10083
10263
  });
10084
10264
 
10085
10265
  // src/report/write.ts
10086
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
10266
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
10087
10267
  import { tmpdir } from "node:os";
10088
- import { dirname as dirname4, join as join10, resolve } from "node:path";
10268
+ import { dirname as dirname4, join as join11, resolve } from "node:path";
10089
10269
  import { pathToFileURL } from "node:url";
10090
10270
  function defaultReportPath() {
10091
- return join10(tmpdir(), "dosu-knowledge-report.html");
10271
+ return join11(tmpdir(), "dosu-knowledge-report.html");
10092
10272
  }
10093
10273
  async function writeAndOpenReport(options) {
10094
10274
  const out = resolve(options.out ?? defaultReportPath());
10095
- mkdirSync7(dirname4(out), { recursive: true });
10096
- writeFileSync7(out, options.html, "utf8");
10275
+ mkdirSync8(dirname4(out), { recursive: true });
10276
+ writeFileSync8(out, options.html, "utf8");
10097
10277
  if (options.open !== false) {
10098
10278
  const url = pathToFileURL(out).href;
10099
10279
  if (options.openUrl) {
@@ -10108,17 +10288,26 @@ async function writeAndOpenReport(options) {
10108
10288
  var init_write = () => {};
10109
10289
 
10110
10290
  // src/report/generate.ts
10291
+ import { basename as basename2 } from "node:path";
10111
10292
  function sessionsForNotes(notes, sessions) {
10112
10293
  const ids = new Set(notes.map((n2) => n2.transcript_id).filter((id) => Boolean(id)));
10113
10294
  return sessions.filter((s) => ids.has(s.id));
10114
10295
  }
10296
+ function defaultProjectName() {
10297
+ const resolver = createProjectDirResolver();
10298
+ return (session) => {
10299
+ const dir = resolver.resolve(session);
10300
+ return dir ? basename2(dir) : null;
10301
+ };
10302
+ }
10115
10303
  function noteTime(note) {
10116
10304
  const parsed = Date.parse(note.at ?? "");
10117
10305
  return Number.isNaN(parsed) ? 0 : parsed;
10118
10306
  }
10119
10307
  async function emitKnowledgeReport(options = {}) {
10120
10308
  const cfg = loadConfig();
10121
- const notes = [...options.notes ?? await (options.fetchNotes ?? fetchReportNotes)(cfg)].sort((a3, b2) => noteTime(a3) - noteTime(b2));
10309
+ const fetched = options.notes ? { notes: options.notes, truncated: false } : await (options.fetchNotes ?? fetchReportNotes)(cfg);
10310
+ const notes = [...fetched.notes].sort((a3, b2) => noteTime(a3) - noteTime(b2));
10122
10311
  const scanned = options.sessions ?? scanAgentSessions();
10123
10312
  const sessions = sessionsForNotes(notes, scanned);
10124
10313
  const candidates = attributeRediscovery(notes, sessions);
@@ -10127,12 +10316,16 @@ async function emitKnowledgeReport(options = {}) {
10127
10316
  if (inventory.totals && state.total_learning_tokens && !options.sessions) {
10128
10317
  inventory.totals.learning_tokens = state.total_learning_tokens;
10129
10318
  }
10319
+ const projectName = options.projectName ?? defaultProjectName();
10320
+ const projects = [
10321
+ ...new Set(sessions.map((s) => projectName(s) ?? s.project).filter((name) => Boolean(name)))
10322
+ ];
10130
10323
  const html = buildReportHtml({
10131
10324
  inventory,
10132
10325
  candidates,
10133
10326
  orgName: options.orgName ?? cfg.active_account?.target?.org_name ?? "Your team",
10134
- repo: options.repo ?? notes.find((n2) => n2.repo)?.repo,
10135
- branch: options.branch ?? notes.find((n2) => n2.branch)?.branch,
10327
+ projects,
10328
+ truncated: fetched.truncated,
10136
10329
  generatedAt: options.generatedAt,
10137
10330
  digests: digestsForSessions(sessions)
10138
10331
  });
@@ -10145,6 +10338,7 @@ async function emitKnowledgeReport(options = {}) {
10145
10338
  }
10146
10339
  var init_generate = __esm(() => {
10147
10340
  init_config();
10341
+ init_project_dir();
10148
10342
  init_scan();
10149
10343
  init_watermark();
10150
10344
  init_html();
@@ -10152,151 +10346,6 @@ var init_generate = __esm(() => {
10152
10346
  init_write();
10153
10347
  });
10154
10348
 
10155
- // src/sessions/project-dir.ts
10156
- import {
10157
- closeSync as closeSync2,
10158
- existsSync as existsSync11,
10159
- mkdirSync as mkdirSync8,
10160
- openSync as openSync2,
10161
- readFileSync as readFileSync12,
10162
- readSync,
10163
- renameSync as renameSync3,
10164
- statSync as statSync4,
10165
- writeFileSync as writeFileSync8
10166
- } from "node:fs";
10167
- import { join as join11 } from "node:path";
10168
- function cachePath(configDir) {
10169
- return join11(configDir, CACHE_FILENAME2);
10170
- }
10171
- function loadCacheFile(configDir) {
10172
- try {
10173
- const raw = JSON.parse(readFileSync12(cachePath(configDir), "utf-8"));
10174
- if (raw.schema_version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== "object")
10175
- return {};
10176
- return raw.entries;
10177
- } catch {
10178
- return {};
10179
- }
10180
- }
10181
- function saveCacheFile(configDir, entries) {
10182
- try {
10183
- if (!existsSync11(configDir))
10184
- mkdirSync8(configDir, { recursive: true, mode: 448 });
10185
- const path3 = cachePath(configDir);
10186
- const tmp = `${path3}.${process.pid}.tmp`;
10187
- const file = { schema_version: CACHE_SCHEMA_VERSION, entries };
10188
- writeFileSync8(tmp, JSON.stringify(file), { mode: 384 });
10189
- renameSync3(tmp, path3);
10190
- } catch {}
10191
- }
10192
- function readHead(path3, bytes = HEAD_BYTES) {
10193
- let fd;
10194
- try {
10195
- fd = openSync2(path3, "r");
10196
- } catch {
10197
- return null;
10198
- }
10199
- try {
10200
- const buf = Buffer.alloc(bytes);
10201
- const read = readSync(fd, buf, 0, bytes, 0);
10202
- return buf.subarray(0, read).toString("utf-8");
10203
- } catch {
10204
- return null;
10205
- } finally {
10206
- closeSync2(fd);
10207
- }
10208
- }
10209
- function cwdFromJsonlHead(text) {
10210
- for (const line of text.split(`
10211
- `).slice(0, HEAD_LINES)) {
10212
- if (!line.includes('"cwd"'))
10213
- continue;
10214
- try {
10215
- const obj = JSON.parse(line);
10216
- const cwd = obj.cwd ?? obj.payload?.cwd;
10217
- if (typeof cwd === "string" && cwd.startsWith("/"))
10218
- return cwd;
10219
- } catch {}
10220
- }
10221
- return null;
10222
- }
10223
- function unmungeSlug(slug, exists = existsSync11) {
10224
- const tokens = slug.replace(/^-/, "").split("-");
10225
- if (tokens.length === 0 || tokens[0] === "")
10226
- return null;
10227
- let budget = 5000;
10228
- const walk = (prefix, index) => {
10229
- if (budget-- <= 0)
10230
- return null;
10231
- if (index === tokens.length)
10232
- return exists(prefix) ? prefix : null;
10233
- const asSegment = prefix === "" || exists(prefix) ? walk(`${prefix}/${tokens[index]}`, index + 1) : null;
10234
- if (asSegment)
10235
- return asSegment;
10236
- return prefix === "" ? null : walk(`${prefix}-${tokens[index]}`, index + 1);
10237
- };
10238
- return walk("", 0);
10239
- }
10240
- function fileMtime(path3) {
10241
- try {
10242
- return statSync4(path3).mtime.toISOString();
10243
- } catch {
10244
- return "";
10245
- }
10246
- }
10247
- function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
10248
- const exists = deps.exists ?? existsSync11;
10249
- const head = deps.readHead ?? readHead;
10250
- const mtime = deps.mtime ?? fileMtime;
10251
- const entries = loadCacheFile(configDir);
10252
- let dirty = false;
10253
- const compute = (session) => {
10254
- switch (session.harness) {
10255
- case "opencode":
10256
- return session.project ?? null;
10257
- case "claude": {
10258
- const text = head(session.path);
10259
- const cwd = text ? cwdFromJsonlHead(text) : null;
10260
- if (cwd)
10261
- return cwd;
10262
- return session.project ? unmungeSlug(session.project, exists) : null;
10263
- }
10264
- case "codex": {
10265
- const text = head(session.path);
10266
- return text ? cwdFromJsonlHead(text) : null;
10267
- }
10268
- case "cursor":
10269
- return session.project ? unmungeSlug(session.project, exists) : null;
10270
- default:
10271
- return null;
10272
- }
10273
- };
10274
- return {
10275
- resolve(session) {
10276
- const key = `${session.harness}/${session.id}`;
10277
- const cached = entries[key];
10278
- if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
10279
- return cached.dir;
10280
- }
10281
- const dir = compute(session);
10282
- entries[key] = { dir, mtime: mtime(session.path) };
10283
- dirty = true;
10284
- return dir;
10285
- },
10286
- flush() {
10287
- if (!dirty)
10288
- return;
10289
- saveCacheFile(configDir, entries);
10290
- dirty = false;
10291
- }
10292
- };
10293
- }
10294
- var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
10295
- var init_project_dir = __esm(() => {
10296
- init_config();
10297
- HEAD_BYTES = 128 * 1024;
10298
- });
10299
-
10300
10349
  // src/sync/backlog.ts
10301
10350
  function listSessionBacklog() {
10302
10351
  try {
@@ -10441,7 +10490,7 @@ function readLockFile(configDir) {
10441
10490
  }
10442
10491
  function recentSyncActivity(log3) {
10443
10492
  return log3.split(`
10444
- `).filter((line) => line.includes("[sync]") || line.includes("[miner]")).slice(-ACTIVITY_LINES).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
10493
+ `).filter((line) => line.includes("[sync]") || line.includes("[learner]")).slice(-ACTIVITY_LINES).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
10445
10494
  }
10446
10495
  function getSyncStatus(deps = {}) {
10447
10496
  const configDir = deps.configDir ?? getConfigDir();
@@ -10490,7 +10539,7 @@ async function runKnowledgeSync(options = {}) {
10490
10539
  const state = loadState();
10491
10540
  if (options.quiet) {
10492
10541
  if (state.paused) {
10493
- logger.debug("sync", "skipping quiet sync: mining is paused");
10542
+ logger.debug("sync", "skipping quiet sync: studying is paused");
10494
10543
  return { status: "skipped-paused", readySessions: 0, inFlightSessions: 0, sessions: [] };
10495
10544
  }
10496
10545
  const retryAt = backoffUntil(state);
@@ -10500,7 +10549,7 @@ async function runKnowledgeSync(options = {}) {
10500
10549
  }
10501
10550
  } else if (state.paused) {
10502
10551
  delete state.paused;
10503
- logger.debug("sync", "manual sync resumes paused mining");
10552
+ logger.debug("sync", "manual sync resumes paused studying");
10504
10553
  }
10505
10554
  let ready;
10506
10555
  let open2;
@@ -10562,14 +10611,14 @@ async function runKnowledgeSync(options = {}) {
10562
10611
  baseline_mined: state.total_mined ?? 0
10563
10612
  };
10564
10613
  }
10565
- const worthMining = deps.worthMining ?? isWorthMining;
10614
+ const worthStudying = deps.worthStudying ?? isWorthStudying;
10566
10615
  const examined = [];
10567
10616
  const batch = [];
10568
10617
  let trivial = 0;
10569
10618
  for (let i2 = ready.length - 1;i2 >= 0 && batch.length < MINE_BATCH_LIMIT; i2--) {
10570
10619
  const candidate = ready[i2];
10571
10620
  examined.push(candidate);
10572
- if (worthMining(candidate)) {
10621
+ if (worthStudying(candidate)) {
10573
10622
  batch.push(candidate);
10574
10623
  } else {
10575
10624
  trivial += 1;
@@ -10586,26 +10635,26 @@ async function runKnowledgeSync(options = {}) {
10586
10635
  return {
10587
10636
  status: "nothing-new",
10588
10637
  ...base,
10589
- minedSessions: 0,
10638
+ studiedSessions: 0,
10590
10639
  trivialSessions: trivial
10591
10640
  };
10592
10641
  }
10593
- logger.debug("sync", `mining ${batch.length} of ${ready.length} ready sessions (${trivial} trivial skipped)`);
10594
- const miner = await deps.mine(batch);
10595
- switch (miner.outcome) {
10642
+ logger.debug("sync", `studying ${batch.length} of ${ready.length} ready sessions (${trivial} trivial skipped)`);
10643
+ const learner = await deps.mine(batch);
10644
+ switch (learner.outcome) {
10596
10645
  case "completed": {
10597
10646
  for (const s of batch) {
10598
- logger.debug("sync", `mined session ${s.harness}/${s.id}`);
10647
+ logger.debug("sync", `studied session ${s.harness}/${s.id}`);
10599
10648
  }
10600
- const minedAt = now().toISOString();
10649
+ const studiedAt = now().toISOString();
10601
10650
  const history = [
10602
10651
  ...state.mined_sessions ?? [],
10603
10652
  ...batch.map((s) => ({
10604
- at: minedAt,
10653
+ at: studiedAt,
10605
10654
  session: `${s.harness}/${s.id}`,
10606
10655
  ...s.project ? { project: s.project } : {}
10607
10656
  }))
10608
- ].slice(-MINED_HISTORY_LIMIT);
10657
+ ].slice(-STUDIED_HISTORY_LIMIT);
10609
10658
  const watermark = batchWatermark(examined);
10610
10659
  const sessionTokens = deps.sessionTokens ?? estimateSessionTokens;
10611
10660
  let batchTokens = 0;
@@ -10614,21 +10663,21 @@ async function runKnowledgeSync(options = {}) {
10614
10663
  saveState({
10615
10664
  ...state,
10616
10665
  watermark,
10617
- last_attempt_at: minedAt,
10666
+ last_attempt_at: studiedAt,
10618
10667
  consecutive_failures: 0,
10619
10668
  mined_sessions: history,
10620
10669
  total_mined: (state.total_mined ?? 0) + batch.length,
10621
- total_notes: (state.total_notes ?? 0) + miner.notesWritten,
10670
+ total_notes: (state.total_notes ?? 0) + learner.notesWritten,
10622
10671
  total_learning_tokens: (state.total_learning_tokens ?? 0) + batchTokens,
10623
10672
  last_refusal: undefined
10624
10673
  });
10625
- logger.debug("sync", `mined ${batch.length} sessions, ${miner.notesWritten} suggested pages; watermark → ${watermark}`);
10674
+ logger.debug("sync", `studied ${batch.length} sessions, ${learner.notesWritten} suggested pages; watermark → ${watermark}`);
10626
10675
  return {
10627
- status: "mined",
10676
+ status: "studied",
10628
10677
  ...base,
10629
- minedSessions: batch.length,
10678
+ studiedSessions: batch.length,
10630
10679
  trivialSessions: trivial,
10631
- miner
10680
+ learner
10632
10681
  };
10633
10682
  }
10634
10683
  case "consent_off":
@@ -10641,12 +10690,12 @@ async function runKnowledgeSync(options = {}) {
10641
10690
  consecutive_failures: 0,
10642
10691
  last_refusal: {
10643
10692
  at,
10644
- outcome: miner.outcome,
10645
- message: miner.message ?? "Mining unavailable right now."
10693
+ outcome: learner.outcome,
10694
+ message: learner.message ?? "Studying unavailable right now."
10646
10695
  }
10647
10696
  });
10648
- logger.debug("sync", `mining skipped by gateway: ${miner.outcome}`);
10649
- return { status: "skipped-gateway", ...base, minedSessions: 0, miner };
10697
+ logger.debug("sync", `studying skipped by gateway: ${learner.outcome}`);
10698
+ return { status: "skipped-gateway", ...base, studiedSessions: 0, learner };
10650
10699
  }
10651
10700
  default: {
10652
10701
  saveState({
@@ -10654,13 +10703,13 @@ async function runKnowledgeSync(options = {}) {
10654
10703
  last_attempt_at: now().toISOString(),
10655
10704
  consecutive_failures: state.consecutive_failures + 1
10656
10705
  });
10657
- logger.debug("sync", `mining failed: ${miner.outcome}; ${miner.message ?? ""}`);
10706
+ logger.debug("sync", `studying failed: ${learner.outcome}; ${learner.message ?? ""}`);
10658
10707
  return {
10659
10708
  status: "mine-failed",
10660
10709
  ...base,
10661
- minedSessions: 0,
10662
- miner,
10663
- error: miner.message
10710
+ studiedSessions: 0,
10711
+ learner,
10712
+ error: learner.message
10664
10713
  };
10665
10714
  }
10666
10715
  }
@@ -10806,12 +10855,12 @@ var init_backfill_run = __esm(() => {
10806
10855
  init_backfill();
10807
10856
  });
10808
10857
 
10809
- // src/miner/config-dir.ts
10858
+ // src/learner/config-dir.ts
10810
10859
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
10811
10860
  import { tmpdir as tmpdir2 } from "node:os";
10812
10861
  import { join as join13 } from "node:path";
10813
10862
  function createRunConfigDir() {
10814
- const path3 = mkdtempSync(join13(tmpdir2(), "dosu-miner-"));
10863
+ const path3 = mkdtempSync(join13(tmpdir2(), "dosu-learner-"));
10815
10864
  writeFileSync10(join13(path3, ".claude.json"), JSON.stringify({ hasCompletedOnboarding: true }));
10816
10865
  return {
10817
10866
  path: path3,
@@ -10822,7 +10871,7 @@ function createRunConfigDir() {
10822
10871
  }
10823
10872
  var init_config_dir = () => {};
10824
10873
 
10825
- // src/miner/conflicts.ts
10874
+ // src/learner/conflicts.ts
10826
10875
  import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
10827
10876
  function managedSettingsPaths(platform3 = process.platform) {
10828
10877
  switch (platform3) {
@@ -10880,11 +10929,11 @@ var init_conflicts = __esm(() => {
10880
10929
  CONFLICTING_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_CODE_", "AWS_"];
10881
10930
  });
10882
10931
 
10883
- // src/miner/env.ts
10932
+ // src/learner/env.ts
10884
10933
  function isStripped(name) {
10885
10934
  return STRIPPED_NAMES.has(name) || STRIPPED_PREFIXES.some((p2) => name.startsWith(p2));
10886
10935
  }
10887
- function buildMinerEnv(options) {
10936
+ function buildLearnerEnv(options) {
10888
10937
  const base = options.baseEnv ?? process.env;
10889
10938
  const env = {};
10890
10939
  for (const [name, value] of Object.entries(base)) {
@@ -10914,7 +10963,7 @@ var init_env = __esm(() => {
10914
10963
  STRIPPED_NAMES = new Set(["CLAUDE_CONFIG_DIR", "CLAUDECODE", "AWS_BEARER_TOKEN_BEDROCK"]);
10915
10964
  });
10916
10965
 
10917
- // src/miner/executable.ts
10966
+ // src/learner/executable.ts
10918
10967
  import { accessSync, constants, existsSync as existsSync14 } from "node:fs";
10919
10968
  import { createRequire as createRequire3 } from "node:module";
10920
10969
  import { homedir as homedir4 } from "node:os";
@@ -10961,9 +11010,9 @@ function resolveClaudeExecutable(options = {}) {
10961
11010
  }
10962
11011
  var init_executable = () => {};
10963
11012
 
10964
- // src/miner/prompt.ts
10965
- function buildMinerSystemPrompt(coreRules) {
10966
- return `You are Dosu's knowledge miner. You read a developer's recent coding-agent sessions and save the durable, non-obvious findings to the team's shared knowledge base so future teammates and agents do not have to rediscover them.
11013
+ // src/learner/prompt.ts
11014
+ function buildLearnerSystemPrompt(coreRules) {
11015
+ return `You are Dosu's knowledge learner. You read a developer's recent coding-agent sessions and save the durable, non-obvious findings to the team's shared knowledge base so future teammates and agents do not have to rediscover them.
10967
11016
 
10968
11017
  Available tools (the only tools you may use, by these exact names):
10969
11018
  - mcp__sessions__list_sessions / mcp__sessions__read_session: the session transcripts in scope for this run.
@@ -10971,7 +11020,7 @@ Available tools (the only tools you may use, by these exact names):
10971
11020
 
10972
11021
  ${coreRules}`;
10973
11022
  }
10974
- function buildMinerPrompt(sessions) {
11023
+ function buildLearnerPrompt(sessions) {
10975
11024
  const list = sessions.map((s) => `- ${s.id} (${s.harness})`).join(`
10976
11025
  `);
10977
11026
  return `Mine the following ${sessions.length} coding-agent session(s) for durable knowledge:
@@ -10981,8 +11030,8 @@ ${list}
10981
11030
  Work through them ONE AT A TIME: read a single session with read_session, decide what (if anything) is durable per your rules, dedupe against read_knowledge, and write that session's notes with write_knowledge BEFORE moving on to the next session. Each note is attributed to the session you read just before writing it, so do not read the next session until you have written this one's notes. When you are done, reply with a one-line summary: how many sessions you read and how many notes you wrote.`;
10982
11031
  }
10983
11032
 
10984
- // src/miner/prompt-core.ts
10985
- var MINER_CORE_RULES = `Rules — non-negotiable:
11033
+ // src/learner/prompt-core.ts
11034
+ var LEARNER_CORE_RULES = `Rules — non-negotiable:
10986
11035
  1. Before writing anything, call read_knowledge with the candidate topic. If the same fact is
10987
11036
  already recorded, skip it — never write a duplicate or near-duplicate. If the topic exists but
10988
11037
  the session shows it has changed or been superseded, do write: a note stating the current state
@@ -11010,10 +11059,9 @@ useful.
11010
11059
  6. Only pass repo/branch to write_knowledge when the session itself verifies them (an explicit
11011
11060
  cwd, git remote, or branch mentioned in the transcript). Never infer or guess a repo. When not
11012
11061
  verified, omit both.
11013
- 7. Populate write_knowledge metadata with source_agent and session_id for every note.
11014
- 8. Never quote credentials, tokens, or secrets — even redacted placeholders — and never include
11062
+ 7. Never quote credentials, tokens, or secrets — even redacted placeholders — and never include
11015
11063
  long verbatim transcript spans. Summarize in your own words.
11016
- 9. A trivial session with no real user query is normal: skip it silently.`;
11064
+ 8. A trivial session with no real user query is normal: skip it silently.`;
11017
11065
 
11018
11066
  // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
11019
11067
  var exports_sdk = {};
@@ -72764,7 +72812,7 @@ var init_redact = __esm(() => {
72764
72812
  ]);
72765
72813
  });
72766
72814
 
72767
- // src/miner/tools.ts
72815
+ // src/learner/tools.ts
72768
72816
  function textResult(text) {
72769
72817
  return { content: [{ type: "text", text }] };
72770
72818
  }
@@ -72823,7 +72871,7 @@ function createSessionToolsServer(sessions) {
72823
72871
  name: SESSIONS_SERVER_NAME,
72824
72872
  version: "1.0.0",
72825
72873
  tools: [
72826
- O6e("list_sessions", "List the coding-agent sessions in scope for this mining run.", {}, async () => textResult(formatSessionList(sessions))),
72874
+ O6e("list_sessions", "List the coding-agent sessions in scope for this study run.", {}, async () => textResult(formatSessionList(sessions))),
72827
72875
  O6e("read_session", "Read the conversation turns of one in-scope session. Responses are " + "paginated; pass the returned offset to continue reading.", {
72828
72876
  id: exports_external.string().describe("Session id from list_sessions"),
72829
72877
  offset: exports_external.number().int().min(0).optional().describe("Turn index to start from")
@@ -72849,11 +72897,11 @@ var init_tools = __esm(() => {
72849
72897
  init_redact();
72850
72898
  });
72851
72899
 
72852
- // src/miner/runner.ts
72900
+ // src/learner/runner.ts
72853
72901
  var exports_runner = {};
72854
72902
  __export(exports_runner, {
72855
72903
  traceAgentMessage: () => traceAgentMessage,
72856
- runMiner: () => runMiner,
72904
+ runLearner: () => runLearner,
72857
72905
  classifyGatewayError: () => classifyGatewayError
72858
72906
  });
72859
72907
  function classifyGatewayError(text) {
@@ -72875,12 +72923,12 @@ function traceAgentMessage(message) {
72875
72923
  return;
72876
72924
  for (const block2 of content) {
72877
72925
  if (msg.type === "assistant" && block2.type === "text") {
72878
- logger.debug("miner", `[agent] ${snippet(block2.text)}`);
72926
+ logger.debug("learner", `[agent] ${snippet(block2.text)}`);
72879
72927
  } else if (msg.type === "assistant" && block2.type === "tool_use") {
72880
- logger.debug("miner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72928
+ logger.debug("learner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72881
72929
  } else if (msg.type === "user" && block2.type === "tool_result") {
72882
72930
  const full = JSON.stringify(block2.content ?? "");
72883
- logger.debug("miner", `[agent] ← result ${full.length} chars${block2.is_error ? " (error)" : ""}: ${snippet(block2.content).slice(0, 120)}`);
72931
+ logger.debug("learner", `[agent] ← result ${full.length} chars${block2.is_error ? " (error)" : ""}: ${snippet(block2.content).slice(0, 120)}`);
72884
72932
  }
72885
72933
  }
72886
72934
  }
@@ -72893,7 +72941,7 @@ function allowedToolNames() {
72893
72941
  `mcp__${KNOWLEDGE_SERVER_NAME}__finalize_session_knowledge`
72894
72942
  ]);
72895
72943
  }
72896
- async function runMiner(options) {
72944
+ async function runLearner(options) {
72897
72945
  const conflicts = detectSettingsConflicts();
72898
72946
  if (conflicts.length > 0) {
72899
72947
  const detail = conflicts.map((c5) => `${c5.file} (${c5.keys.join(", ")})`).join("; ");
@@ -72901,7 +72949,7 @@ async function runMiner(options) {
72901
72949
  outcome: "settings_conflict",
72902
72950
  notesWritten: 0,
72903
72951
  turns: 0,
72904
- message: `Refusing to run: conflicting Claude Code settings would override the miner's auth (${detail})`
72952
+ message: `Refusing to run: conflicting Claude Code settings would override the study run's auth (${detail})`
72905
72953
  };
72906
72954
  }
72907
72955
  const gatewayURL = options.gatewayURL ?? getLlmGatewayURL();
@@ -72916,7 +72964,7 @@ async function runMiner(options) {
72916
72964
  const { query } = await Promise.resolve().then(() => (init_sdk(), exports_sdk));
72917
72965
  const claudeExecutable = resolveClaudeExecutable();
72918
72966
  if (claudeExecutable) {
72919
- logger.debug("miner", `using system Claude Code executable: ${claudeExecutable}`);
72967
+ logger.debug("learner", `using system Claude Code executable: ${claudeExecutable}`);
72920
72968
  }
72921
72969
  const configDir = createRunConfigDir();
72922
72970
  const runID = options.runID ?? crypto.randomUUID();
@@ -72929,7 +72977,7 @@ async function runMiner(options) {
72929
72977
  let currentSession;
72930
72978
  const readsSinceWrite = new Set;
72931
72979
  let turns = 0;
72932
- const env = buildMinerEnv({
72980
+ const env = buildLearnerEnv({
72933
72981
  apiKey: options.apiKey,
72934
72982
  gatewayURL,
72935
72983
  configDir: configDir.path,
@@ -72940,9 +72988,9 @@ async function runMiner(options) {
72940
72988
  });
72941
72989
  try {
72942
72990
  const run2 = query({
72943
- prompt: buildMinerPrompt(options.sessions),
72991
+ prompt: buildLearnerPrompt(options.sessions),
72944
72992
  options: {
72945
- systemPrompt: buildMinerSystemPrompt(MINER_CORE_RULES),
72993
+ systemPrompt: buildLearnerSystemPrompt(LEARNER_CORE_RULES),
72946
72994
  env,
72947
72995
  abortController: abort,
72948
72996
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
@@ -72958,17 +73006,17 @@ async function runMiner(options) {
72958
73006
  headers: {
72959
73007
  ...mcpHeaders(options.apiKey),
72960
73008
  "X-Dosu-Session-Id": runID,
72961
- "X-Dosu-Client": `dosu-cli-miner/${getVersionString()}`,
73009
+ "X-Dosu-Client": `dosu-cli-learner/${getVersionString()}`,
72962
73010
  ...sessionStartedAt ? { "X-Dosu-Session-Started-At": sessionStartedAt } : {}
72963
73011
  }
72964
73012
  }
72965
73013
  },
72966
- stderr: (data) => logger.debug("miner", `[sdk] ${data}`),
73014
+ stderr: (data) => logger.debug("learner", `[sdk] ${data}`),
72967
73015
  canUseTool: async (toolName, input2) => {
72968
73016
  if (!allowed.has(toolName)) {
72969
73017
  return {
72970
73018
  behavior: "deny",
72971
- message: `Tool ${toolName} is not permitted in mining runs.`
73019
+ message: `Tool ${toolName} is not permitted in study runs.`
72972
73020
  };
72973
73021
  }
72974
73022
  if (toolName === `mcp__${SESSIONS_SERVER_NAME}__read_session`) {
@@ -73020,15 +73068,15 @@ async function runMiner(options) {
73020
73068
  };
73021
73069
  }
73022
73070
  if (message.subtype !== "success" || message.is_error) {
73023
- logger.debug("miner", `run ${runID} failed: ${text}`);
73071
+ logger.debug("learner", `run ${runID} failed: ${text}`);
73024
73072
  return {
73025
73073
  outcome: "error",
73026
73074
  notesWritten,
73027
73075
  turns,
73028
- message: "Mining run failed; see debug log for details."
73076
+ message: "Study run failed; see debug log for details."
73029
73077
  };
73030
73078
  }
73031
- logger.debug("miner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73079
+ logger.debug("learner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73032
73080
  return { outcome: "completed", notesWritten, turns, message: text };
73033
73081
  }
73034
73082
  }
@@ -73036,7 +73084,7 @@ async function runMiner(options) {
73036
73084
  outcome: "error",
73037
73085
  notesWritten,
73038
73086
  turns,
73039
- message: "Mining run ended without a result."
73087
+ message: "Study run ended without a result."
73040
73088
  };
73041
73089
  } catch (error61) {
73042
73090
  const text = error61 instanceof Error ? error61.message : String(error61);
@@ -73044,12 +73092,12 @@ async function runMiner(options) {
73044
73092
  if (gatewayError) {
73045
73093
  return { outcome: gatewayError.outcome, notesWritten, turns, message: gatewayError.message };
73046
73094
  }
73047
- logger.debug("miner", `run ${runID} threw: ${text}`);
73095
+ logger.debug("learner", `run ${runID} threw: ${text}`);
73048
73096
  return {
73049
73097
  outcome: "error",
73050
73098
  notesWritten,
73051
73099
  turns,
73052
- message: abort.signal.aborted ? "Mining run timed out and was aborted." : "Mining run failed; see debug log for details."
73100
+ message: abort.signal.aborted ? "Study run timed out and was aborted." : "Study run failed; see debug log for details."
73053
73101
  };
73054
73102
  } finally {
73055
73103
  clearTimeout(timer);
@@ -73519,7 +73567,7 @@ var init_settings = __esm(() => {
73519
73567
  });
73520
73568
 
73521
73569
  // src/telemetry/debug-id.ts
73522
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "971484cf-da0f-e0c4-6475-6e2164756e21";
73570
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "e98d2bf5-99f8-402d-6475-6e2164756e21";
73523
73571
 
73524
73572
  // src/telemetry/telemetry.ts
73525
73573
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -73548,11 +73596,13 @@ function normalizeContext(context) {
73548
73596
  const isAuthenticated2 = context?.isAuthenticated === true;
73549
73597
  const id2 = safeRead(context?.user, "id");
73550
73598
  const email3 = safeRead(context?.user, "email");
73551
- const user = isAuthenticated2 && validUserId(id2) ? { id: id2, ...validEmail(email3) ? { email: email3 } : {} } : undefined;
73599
+ const orgId = safeRead(context, "orgId");
73600
+ const user = isAuthenticated2 && isUUID2(id2) ? { id: id2, ...validEmail(email3) ? { email: email3 } : {} } : undefined;
73552
73601
  return {
73553
73602
  mode: context?.mode === "oss" ? "oss" : "cloud",
73554
73603
  isAuthenticated: isAuthenticated2,
73555
- ...user ? { user } : {}
73604
+ ...user ? { user } : {},
73605
+ ...user && isUUID2(orgId) ? { orgId } : {}
73556
73606
  };
73557
73607
  }
73558
73608
  function normalizeExitCode(value, fallback = 1) {
@@ -73572,12 +73622,9 @@ function normalizeRuntime(runtime) {
73572
73622
  isTty: runtime.isTty === true
73573
73623
  };
73574
73624
  }
73575
- function validInstallId(value) {
73625
+ function isUUID2(value) {
73576
73626
  return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
73577
73627
  }
73578
- function validUserId(value) {
73579
- return validInstallId(value);
73580
- }
73581
73628
  function validEmail(value) {
73582
73629
  return typeof value === "string" && value.length <= 254 && /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/.test(value);
73583
73630
  }
@@ -73727,7 +73774,7 @@ function buildPostHogPayload(input2) {
73727
73774
  const result = input2.result === "success" || input2.result === "validation_error" ? input2.result : "failure";
73728
73775
  return {
73729
73776
  api_key: input2.apiKey,
73730
- distinct_id: context.user?.id ?? (validInstallId(input2.installId) ? input2.installId : FALLBACK_INSTALL_ID),
73777
+ distinct_id: context.user?.id ?? (isUUID2(input2.installId) ? input2.installId : FALLBACK_INSTALL_ID),
73731
73778
  event: "cli_command_completed",
73732
73779
  properties: {
73733
73780
  $geoip_disable: true,
@@ -73745,6 +73792,10 @@ function buildPostHogPayload(input2) {
73745
73792
  is_ci: runtime.isCi,
73746
73793
  is_tty: runtime.isTty,
73747
73794
  mode: context.mode,
73795
+ ...context.orgId ? {
73796
+ org_id: context.orgId,
73797
+ $groups: { organization: context.orgId }
73798
+ } : {},
73748
73799
  is_authenticated: context.isAuthenticated,
73749
73800
  exit_code: normalizeExitCode(input2.exitCode),
73750
73801
  ...errorCode ? { error_code: errorCode } : {}
@@ -73859,7 +73910,12 @@ function buildSentryEnvelope(input2) {
73859
73910
  level: "error",
73860
73911
  release: `dosu-cli@${runtime.version}`,
73861
73912
  tags: sentryTags(command, context, runtime, error61),
73862
- ...context.user ? { user: context.user } : {},
73913
+ ...context.user ? {
73914
+ user: {
73915
+ id: context.user.id,
73916
+ ...context.user.email ? { email: context.user.email } : {}
73917
+ }
73918
+ } : {},
73863
73919
  ...mappedBundle ? {
73864
73920
  debug_meta: {
73865
73921
  images: [{ type: "sourcemap", code_file: BUNDLE_CODE_FILE, debug_id: debugId }]
@@ -74053,7 +74109,7 @@ function safeNow(now) {
74053
74109
  function safeUuid(generate) {
74054
74110
  try {
74055
74111
  const value = generate();
74056
- return validInstallId(value) ? value : undefined;
74112
+ return isUUID2(value) ? value : undefined;
74057
74113
  } catch {
74058
74114
  return;
74059
74115
  }
@@ -74085,7 +74141,7 @@ function createCommandTelemetry(settings2, dependencies = {}) {
74085
74141
  let installIdResolved = false;
74086
74142
  let resolvedInstallId = settings2.install_id;
74087
74143
  function installId() {
74088
- if (validInstallId(resolvedInstallId))
74144
+ if (isUUID2(resolvedInstallId))
74089
74145
  return resolvedInstallId;
74090
74146
  if (installIdResolved)
74091
74147
  return;
@@ -74095,7 +74151,7 @@ function createCommandTelemetry(settings2, dependencies = {}) {
74095
74151
  } catch {
74096
74152
  resolvedInstallId = undefined;
74097
74153
  }
74098
- return validInstallId(resolvedInstallId) ? resolvedInstallId : undefined;
74154
+ return isUUID2(resolvedInstallId) ? resolvedInstallId : undefined;
74099
74155
  }
74100
74156
  function debugPayload(payload) {
74101
74157
  try {
@@ -74314,16 +74370,6 @@ var init_follow = () => {};
74314
74370
 
74315
74371
  // src/mcp/providers/base.ts
74316
74372
  function createJSONProvider(opts) {
74317
- const defaultBuildServer = (cfg) => ({
74318
- type: "http",
74319
- url: mcpURL(cfg.active_account.target.deployment_id),
74320
- headers: mcpHeaders(cfg.active_account?.target?.api_key)
74321
- });
74322
- const defaultBuildOSSServer = (cfg) => ({
74323
- type: "http",
74324
- url: mcpBaseURL(),
74325
- headers: mcpHeaders(cfg.active_account?.target?.api_key)
74326
- });
74327
74373
  const buildServer = opts.buildServer ?? defaultBuildServer;
74328
74374
  return {
74329
74375
  name: () => opts.providerName,
@@ -74335,8 +74381,10 @@ function createJSONProvider(opts) {
74335
74381
  globalConfigPath: () => expandHome(opts.globalPath),
74336
74382
  isConfigured: () => isJSONKeyConfigured(expandHome(opts.globalPath), opts.topKey),
74337
74383
  install(cfg, global2) {
74338
- if (cfg.mode !== MODE_OSS && !cfg.active_account?.target?.deployment_id)
74339
- throw new Error("deployment ID is required");
74384
+ const endpoint = {
74385
+ url: mcpEndpoint(cfg),
74386
+ headers: mcpHeaders(cfg.active_account?.target?.api_key)
74387
+ };
74340
74388
  let configPath;
74341
74389
  if (global2) {
74342
74390
  configPath = expandHome(opts.globalPath);
@@ -74345,8 +74393,7 @@ function createJSONProvider(opts) {
74345
74393
  } else {
74346
74394
  throw new Error(`${opts.providerName} does not support local installation`);
74347
74395
  }
74348
- const serverBuilder = cfg.mode === MODE_OSS ? defaultBuildOSSServer : buildServer;
74349
- installJSONServer(configPath, opts.topKey, serverBuilder(cfg));
74396
+ installJSONServer(configPath, opts.topKey, buildServer(endpoint, cfg));
74350
74397
  },
74351
74398
  remove(global2) {
74352
74399
  let configPath;
@@ -74361,8 +74408,12 @@ function createJSONProvider(opts) {
74361
74408
  }
74362
74409
  };
74363
74410
  }
74411
+ var defaultBuildServer = ({ url: url2, headers }) => ({
74412
+ type: "http",
74413
+ url: url2,
74414
+ headers
74415
+ });
74364
74416
  var init_base = __esm(() => {
74365
- init_config();
74366
74417
  init_config_helpers();
74367
74418
  init_detect();
74368
74419
  });
@@ -74376,13 +74427,12 @@ var AntigravityProvider = () => createJSONProvider({
74376
74427
  paths: ["~/.gemini"],
74377
74428
  globalPath: "~/.gemini/antigravity/mcp_config.json",
74378
74429
  topKey: "mcpServers",
74379
- buildServer: (cfg) => ({
74380
- serverUrl: mcpURL(cfg.active_account.target.deployment_id),
74381
- headers: mcpHeaders(cfg.active_account.target.api_key)
74430
+ buildServer: ({ url: url2, headers }) => ({
74431
+ serverUrl: url2,
74432
+ headers
74382
74433
  })
74383
74434
  });
74384
74435
  var init_antigravity = __esm(() => {
74385
- init_config_helpers();
74386
74436
  init_base();
74387
74437
  });
74388
74438
 
@@ -74419,9 +74469,7 @@ var ClaudeDesktopProvider = () => ({
74419
74469
  install(cfg, global2) {
74420
74470
  if (!global2)
74421
74471
  throw new Error("Claude Desktop does not support local installation");
74422
- if (cfg.mode !== MODE_OSS && !cfg.active_account?.target?.deployment_id)
74423
- throw new Error("deployment ID is required");
74424
- const url2 = cfg.mode === MODE_OSS ? mcpBaseURL() : mcpURL(cfg.active_account.target.deployment_id);
74472
+ const url2 = mcpEndpoint(cfg);
74425
74473
  const npx = findNpx();
74426
74474
  const remote = mcpRemoteServer(url2, cfg.active_account?.target?.api_key);
74427
74475
  installJSONServer(configPath(), "mcpServers", {
@@ -74437,7 +74485,6 @@ var ClaudeDesktopProvider = () => ({
74437
74485
  }
74438
74486
  });
74439
74487
  var init_claude_desktop = __esm(() => {
74440
- init_config();
74441
74488
  init_config_helpers();
74442
74489
  init_detect();
74443
74490
  });
@@ -74452,15 +74499,14 @@ var extensionDir = () => join25(appSupportDir(), "Code", "User", "globalStorage"
74452
74499
  paths: [extensionDir()],
74453
74500
  globalPath: join25(extensionDir(), "settings", "cline_mcp_settings.json"),
74454
74501
  topKey: "mcpServers",
74455
- buildServer: (cfg) => ({
74456
- url: mcpURL(cfg.active_account.target.deployment_id),
74502
+ buildServer: ({ url: url2, headers }) => ({
74503
+ url: url2,
74457
74504
  type: "streamableHttp",
74458
74505
  disabled: false,
74459
- headers: mcpHeaders(cfg.active_account.target.api_key)
74506
+ headers
74460
74507
  })
74461
74508
  });
74462
74509
  var init_cline = __esm(() => {
74463
- init_config_helpers();
74464
74510
  init_detect();
74465
74511
  init_base();
74466
74512
  });
@@ -74478,15 +74524,14 @@ var ClineCliProvider = () => createJSONProvider({
74478
74524
  paths: [clineDir()],
74479
74525
  globalPath: join26(clineDir(), "data", "settings", "cline_mcp_settings.json"),
74480
74526
  topKey: "mcpServers",
74481
- buildServer: (cfg) => ({
74482
- url: mcpURL(cfg.active_account.target.deployment_id),
74527
+ buildServer: ({ url: url2, headers }) => ({
74528
+ url: url2,
74483
74529
  type: "streamableHttp",
74484
74530
  disabled: false,
74485
- headers: mcpHeaders(cfg.active_account.target.api_key)
74531
+ headers
74486
74532
  })
74487
74533
  });
74488
74534
  var init_cline_cli = __esm(() => {
74489
- init_config_helpers();
74490
74535
  init_detect();
74491
74536
  init_base();
74492
74537
  });
@@ -74510,13 +74555,6 @@ function readTOML(path3) {
74510
74555
  function writeTOML(path3, content) {
74511
74556
  writeSecureFile(path3, content);
74512
74557
  }
74513
- function mcpEndpoint(cfg) {
74514
- if (cfg.mode === MODE_OSS)
74515
- return mcpBaseURL();
74516
- if (!cfg.active_account?.target?.deployment_id)
74517
- throw new Error("deployment ID is required");
74518
- return mcpURL(cfg.active_account?.target?.deployment_id);
74519
- }
74520
74558
  function tomlString(value) {
74521
74559
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
74522
74560
  }
@@ -74599,13 +74637,6 @@ function globalPath() {
74599
74637
  }
74600
74638
  return expandHome("~/.copilot/mcp-config.json");
74601
74639
  }
74602
- function mcpEndpoint2(cfg) {
74603
- if (cfg.mode === MODE_OSS)
74604
- return mcpBaseURL();
74605
- if (!cfg.active_account?.target?.deployment_id)
74606
- throw new Error("deployment ID is required");
74607
- return mcpURL(cfg.active_account?.target?.deployment_id);
74608
- }
74609
74640
  var CopilotProvider = () => ({
74610
74641
  name: () => "GitHub Copilot CLI",
74611
74642
  id: () => "copilot",
@@ -74616,7 +74647,7 @@ var CopilotProvider = () => ({
74616
74647
  globalConfigPath: () => globalPath(),
74617
74648
  isConfigured: () => isJSONKeyConfigured(globalPath(), "mcpServers"),
74618
74649
  install(cfg, global2) {
74619
- const url2 = mcpEndpoint2(cfg);
74650
+ const url2 = mcpEndpoint(cfg);
74620
74651
  if (global2) {
74621
74652
  const server = {
74622
74653
  type: "http",
@@ -74645,7 +74676,6 @@ var CopilotProvider = () => ({
74645
74676
  }
74646
74677
  });
74647
74678
  var init_copilot = __esm(() => {
74648
- init_config();
74649
74679
  init_config_helpers();
74650
74680
  init_detect();
74651
74681
  });
@@ -74660,14 +74690,13 @@ var CursorProvider = () => createJSONProvider({
74660
74690
  paths: ["~/.cursor"],
74661
74691
  globalPath: "~/.cursor/mcp.json",
74662
74692
  topKey: "mcpServers",
74663
- buildServer: (cfg) => ({
74664
- url: mcpURL(cfg.active_account.target.deployment_id),
74665
- headers: mcpHeaders(cfg.active_account.target.api_key)
74693
+ buildServer: ({ url: url2, headers }) => ({
74694
+ url: url2,
74695
+ headers
74666
74696
  }),
74667
74697
  localConfigPath: (cwd) => join29(cwd, ".cursor", "mcp.json")
74668
74698
  });
74669
74699
  var init_cursor = __esm(() => {
74670
- init_config_helpers();
74671
74700
  init_base();
74672
74701
  });
74673
74702
 
@@ -74704,13 +74733,6 @@ var init_gemini = __esm(() => {
74704
74733
  });
74705
74734
 
74706
74735
  // src/mcp/providers/manual.ts
74707
- function mcpEndpoint3(cfg) {
74708
- if (cfg.mode === MODE_OSS)
74709
- return mcpBaseURL();
74710
- if (!cfg.active_account?.target?.deployment_id)
74711
- throw new Error("deployment ID is required");
74712
- return mcpURL(cfg.active_account?.target?.deployment_id);
74713
- }
74714
74736
  function maskSecret(secret) {
74715
74737
  if (secret.length <= 8)
74716
74738
  return "[hidden]";
@@ -74722,7 +74744,7 @@ var ManualProvider = () => ({
74722
74744
  id: () => "manual",
74723
74745
  supportsLocal: () => false,
74724
74746
  install(cfg, _global, opts = {}) {
74725
- const url2 = mcpEndpoint3(cfg);
74747
+ const url2 = mcpEndpoint(cfg);
74726
74748
  const apiKey = mcpHeaders(cfg.active_account?.target?.api_key)["X-Dosu-API-Key"];
74727
74749
  const headerValue = opts.showSecret ? apiKey : maskSecret(apiKey);
74728
74750
  console.log("Use these details to configure the Dosu MCP server in your client:");
@@ -74742,7 +74764,6 @@ To remove the Dosu MCP server, manually delete the configuration from your clien
74742
74764
  }
74743
74765
  });
74744
74766
  var init_manual = __esm(() => {
74745
- init_config();
74746
74767
  init_config_helpers();
74747
74768
  });
74748
74769
 
@@ -74758,13 +74779,6 @@ function resolveGlobalConfigPath() {
74758
74779
  return jsoncPath;
74759
74780
  return jsonPath;
74760
74781
  }
74761
- function mcpEndpoint4(cfg) {
74762
- if (cfg.mode === MODE_OSS)
74763
- return mcpBaseURL();
74764
- if (!cfg.active_account?.target?.deployment_id)
74765
- throw new Error("deployment ID is required");
74766
- return mcpURL(cfg.active_account?.target?.deployment_id);
74767
- }
74768
74782
  var MCPorterProvider = () => ({
74769
74783
  name: () => "MCPorter",
74770
74784
  id: () => "mcporter",
@@ -74778,7 +74792,7 @@ var MCPorterProvider = () => ({
74778
74792
  const configPath2 = global2 ? resolveGlobalConfigPath() : join32(process.cwd(), "config", "mcporter.json");
74779
74793
  const server = {
74780
74794
  type: "http",
74781
- url: mcpEndpoint4(cfg),
74795
+ url: mcpEndpoint(cfg),
74782
74796
  headers: mcpHeaders(cfg.active_account.target.api_key)
74783
74797
  };
74784
74798
  installJSONServer(configPath2, "mcpServers", server);
@@ -74789,7 +74803,6 @@ var MCPorterProvider = () => ({
74789
74803
  }
74790
74804
  });
74791
74805
  var init_mcporter = __esm(() => {
74792
- init_config();
74793
74806
  init_config_helpers();
74794
74807
  init_detect();
74795
74808
  });
@@ -74804,16 +74817,15 @@ var OpenCodeProvider = () => createJSONProvider({
74804
74817
  paths: ["~/.config/opencode"],
74805
74818
  globalPath: "~/.config/opencode/opencode.json",
74806
74819
  topKey: "mcp",
74807
- buildServer: (cfg) => ({
74820
+ buildServer: ({ url: url2, headers }) => ({
74808
74821
  type: "remote",
74809
- url: mcpURL(cfg.active_account.target.deployment_id),
74822
+ url: url2,
74810
74823
  enabled: true,
74811
- headers: mcpHeaders(cfg.active_account.target.api_key)
74824
+ headers
74812
74825
  }),
74813
74826
  localConfigPath: (cwd) => join33(cwd, "opencode.json")
74814
74827
  });
74815
74828
  var init_opencode = __esm(() => {
74816
- init_config_helpers();
74817
74829
  init_base();
74818
74830
  });
74819
74831
 
@@ -74851,30 +74863,73 @@ var init_windsurf = __esm(() => {
74851
74863
  });
74852
74864
 
74853
74865
  // src/mcp/providers/zed.ts
74854
- import { platform as platform3 } from "node:os";
74866
+ import { rmSync as rmSync5 } from "node:fs";
74867
+ import { homedir as homedir9, platform as platform3 } from "node:os";
74855
74868
  import { join as join36 } from "node:path";
74856
74869
  function zedConfigDir() {
74857
74870
  const os3 = platform3();
74858
- if (os3 === "darwin" || os3 === "win32")
74871
+ if (os3 === "win32")
74859
74872
  return join36(appSupportDir(), "Zed");
74873
+ if (os3 === "darwin")
74874
+ return join36(homedir9(), ".config", "zed");
74860
74875
  return join36(appSupportDir(), "zed");
74861
74876
  }
74862
- var ZedProvider = () => createJSONProvider({
74863
- providerName: "Zed",
74864
- providerID: "zed",
74865
- local: true,
74866
- priorityValue: 10,
74867
- paths: [zedConfigDir()],
74868
- globalPath: join36(zedConfigDir(), "settings.json"),
74869
- topKey: "context_servers",
74870
- buildServer: (cfg) => ({
74871
- source: "custom",
74872
- type: "http",
74873
- url: mcpURL(cfg.active_account.target.deployment_id),
74874
- headers: mcpHeaders(cfg.active_account.target.api_key)
74875
- }),
74876
- localConfigPath: (cwd) => join36(cwd, ".zed", "settings.json")
74877
- });
74877
+ function zedDataDir() {
74878
+ const os3 = platform3();
74879
+ if (os3 === "darwin")
74880
+ return join36(appSupportDir(), "Zed");
74881
+ if (os3 === "win32")
74882
+ return join36(process.env.LOCALAPPDATA ?? "", "Zed");
74883
+ return join36(process.env.XDG_DATA_HOME ?? join36(homedir9(), ".local", "share"), "zed");
74884
+ }
74885
+ function legacyDarwinSettingsPath() {
74886
+ return platform3() === "darwin" ? join36(appSupportDir(), "Zed", "settings.json") : null;
74887
+ }
74888
+ function removeLegacyDosuEntry(path3) {
74889
+ let cfg;
74890
+ try {
74891
+ cfg = readJSONConfig(path3);
74892
+ } catch {
74893
+ return;
74894
+ }
74895
+ const servers = cfg.context_servers;
74896
+ if (typeof servers !== "object" || servers === null || !("dosu" in servers))
74897
+ return;
74898
+ delete servers.dosu;
74899
+ const nothingElse = Object.keys(cfg).length === 1 && Object.keys(servers).length === 0;
74900
+ if (nothingElse)
74901
+ rmSync5(path3, { force: true });
74902
+ else
74903
+ saveJSONConfig(path3, cfg);
74904
+ }
74905
+ var ZedProvider = (legacyPath = legacyDarwinSettingsPath()) => {
74906
+ const provider = createJSONProvider({
74907
+ providerName: "Zed",
74908
+ providerID: "zed",
74909
+ local: true,
74910
+ priorityValue: 10,
74911
+ paths: [zedConfigDir(), zedDataDir()],
74912
+ globalPath: join36(zedConfigDir(), "settings.json"),
74913
+ topKey: "context_servers",
74914
+ buildServer: ({ url: url2, headers }) => ({ url: url2, headers }),
74915
+ localConfigPath: (cwd) => join36(cwd, ".zed", "settings.json")
74916
+ });
74917
+ if (!legacyPath)
74918
+ return provider;
74919
+ return {
74920
+ ...provider,
74921
+ install(cfg, global2) {
74922
+ provider.install(cfg, global2);
74923
+ if (global2)
74924
+ removeLegacyDosuEntry(legacyPath);
74925
+ },
74926
+ remove(global2) {
74927
+ provider.remove(global2);
74928
+ if (global2)
74929
+ removeLegacyDosuEntry(legacyPath);
74930
+ }
74931
+ };
74932
+ };
74878
74933
  var init_zed = __esm(() => {
74879
74934
  init_config_helpers();
74880
74935
  init_detect();
@@ -75088,13 +75143,13 @@ var init_update_check = __esm(() => {
75088
75143
 
75089
75144
  // src/rules/installer.ts
75090
75145
  import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync22, unlinkSync as unlinkSync4, writeFileSync as writeFileSync14 } from "node:fs";
75091
- import { homedir as homedir9 } from "node:os";
75146
+ import { homedir as homedir10 } from "node:os";
75092
75147
  import { dirname as dirname9, join as join38 } from "node:path";
75093
75148
  function claudeConfigDir() {
75094
- return process.env.CLAUDE_CONFIG_DIR || join38(homedir9(), ".claude");
75149
+ return process.env.CLAUDE_CONFIG_DIR || join38(homedir10(), ".claude");
75095
75150
  }
75096
75151
  function codexHome3() {
75097
- return process.env.CODEX_HOME || join38(homedir9(), ".codex");
75152
+ return process.env.CODEX_HOME || join38(homedir10(), ".codex");
75098
75153
  }
75099
75154
  function isRuleAgent(agent) {
75100
75155
  return Object.hasOwn(RULE_TARGETS, agent);
@@ -75240,7 +75295,7 @@ var init_installer = __esm(() => {
75240
75295
  },
75241
75296
  cursor: {
75242
75297
  kind: "file",
75243
- path: () => join38(homedir9(), ".cursor", "rules", "dosu.mdc"),
75298
+ path: () => join38(homedir10(), ".cursor", "rules", "dosu.mdc"),
75244
75299
  cursorFrontmatter: true
75245
75300
  },
75246
75301
  codex: {
@@ -75249,15 +75304,15 @@ var init_installer = __esm(() => {
75249
75304
  },
75250
75305
  opencode: {
75251
75306
  kind: "section",
75252
- path: () => join38(homedir9(), ".config", "opencode", "AGENTS.md")
75307
+ path: () => join38(homedir10(), ".config", "opencode", "AGENTS.md")
75253
75308
  },
75254
75309
  gemini: {
75255
75310
  kind: "section",
75256
- path: () => join38(homedir9(), ".gemini", "GEMINI.md")
75311
+ path: () => join38(homedir10(), ".gemini", "GEMINI.md")
75257
75312
  },
75258
75313
  antigravity: {
75259
75314
  kind: "section",
75260
- path: () => join38(homedir9(), ".gemini", "GEMINI.md")
75315
+ path: () => join38(homedir10(), ".gemini", "GEMINI.md")
75261
75316
  }
75262
75317
  };
75263
75318
  });
@@ -75395,6 +75450,140 @@ var init_errors3 = __esm(() => {
75395
75450
  // src/mcp/constants.ts
75396
75451
  var MCP_PROVIDER_SLUG2 = "dosu_mcp";
75397
75452
 
75453
+ // src/sessions/session-title.ts
75454
+ import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync24, renameSync as renameSync6, statSync as statSync9, writeFileSync as writeFileSync16 } from "node:fs";
75455
+ import { homedir as homedir11 } from "node:os";
75456
+ import { join as join40 } from "node:path";
75457
+ function cachePath2(configDir) {
75458
+ return join40(configDir, CACHE_FILENAME5);
75459
+ }
75460
+ function loadCacheFile2(configDir) {
75461
+ try {
75462
+ const raw = JSON.parse(readFileSync24(cachePath2(configDir), "utf-8"));
75463
+ if (raw.schema_version !== CACHE_SCHEMA_VERSION2 || typeof raw.entries !== "object")
75464
+ return {};
75465
+ return raw.entries;
75466
+ } catch {
75467
+ return {};
75468
+ }
75469
+ }
75470
+ function saveCacheFile2(configDir, entries) {
75471
+ try {
75472
+ if (!existsSync25(configDir))
75473
+ mkdirSync14(configDir, { recursive: true, mode: 448 });
75474
+ const path3 = cachePath2(configDir);
75475
+ const tmp = `${path3}.${process.pid}.tmp`;
75476
+ const file2 = { schema_version: CACHE_SCHEMA_VERSION2, entries };
75477
+ writeFileSync16(tmp, JSON.stringify(file2), { mode: 384 });
75478
+ renameSync6(tmp, path3);
75479
+ } catch {}
75480
+ }
75481
+ function extractUserQuery(text) {
75482
+ const tagged = text.match(/<user_query>([\s\S]*?)<\/user_query>/);
75483
+ if (tagged)
75484
+ return tagged[1];
75485
+ const named = text.match(/^\s*<[\w-]+\s[^>]*\bname="([^"]+)"/);
75486
+ if (named)
75487
+ return named[1];
75488
+ return text.replace(/^(\s*<([\w-]+)(?:\s[^>]*)?>[\s\S]*?<\/\2>\s*)+/, "");
75489
+ }
75490
+ function cleanSessionName(text) {
75491
+ const line = text.replace(/\s+/g, " ").trim();
75492
+ if (!line)
75493
+ return null;
75494
+ return line.length > SESSION_NAME_MAX ? `${line.slice(0, SESSION_NAME_MAX - 1)}…` : line;
75495
+ }
75496
+ function claudeSummaryFromHead(text) {
75497
+ for (const line of text.split(`
75498
+ `).slice(0, HEAD_LINES2)) {
75499
+ if (!line.includes('"summary"'))
75500
+ continue;
75501
+ try {
75502
+ const obj = JSON.parse(line);
75503
+ if (obj.type === "summary" && typeof obj.summary === "string") {
75504
+ return cleanSessionName(obj.summary);
75505
+ }
75506
+ } catch {}
75507
+ }
75508
+ return null;
75509
+ }
75510
+ function reconstructSession(harness, id2, slug, home = homedir11(), exists = existsSync25) {
75511
+ if (!slug)
75512
+ return null;
75513
+ const candidates = [];
75514
+ if (harness === "claude") {
75515
+ candidates.push(join40(home, ".claude", "projects", slug, `${id2}.jsonl`));
75516
+ } else if (harness === "cursor") {
75517
+ const project = join40(home, ".cursor", "projects", slug);
75518
+ candidates.push(join40(project, "agent-transcripts", id2, `${id2}.jsonl`), join40(project, "agent-transcripts", `${id2}.jsonl`));
75519
+ } else {
75520
+ return null;
75521
+ }
75522
+ for (const path3 of candidates) {
75523
+ if (exists(path3))
75524
+ return { id: id2, harness, path: path3, project: slug, updated: "" };
75525
+ }
75526
+ return null;
75527
+ }
75528
+ function readHead2(path3) {
75529
+ try {
75530
+ return readFileSync24(path3, "utf-8").slice(0, 128 * 1024);
75531
+ } catch {
75532
+ return null;
75533
+ }
75534
+ }
75535
+ function fileMtime2(path3) {
75536
+ try {
75537
+ return statSync9(path3).mtime.toISOString();
75538
+ } catch {
75539
+ return "";
75540
+ }
75541
+ }
75542
+ function createSessionTitleResolver(configDir = getConfigDir(), deps = {}) {
75543
+ const head = deps.readHead ?? readHead2;
75544
+ const turns = deps.readTurns ?? readSessionTurns;
75545
+ const mtime = deps.mtime ?? fileMtime2;
75546
+ const entries = loadCacheFile2(configDir);
75547
+ let dirty = false;
75548
+ const compute = (session) => {
75549
+ if (session.harness === "claude") {
75550
+ const text = head(session.path);
75551
+ const summary = text ? claudeSummaryFromHead(text) : null;
75552
+ if (summary)
75553
+ return summary;
75554
+ }
75555
+ const firstUser = turns(session).find((t9) => t9.role === "user");
75556
+ return firstUser ? cleanSessionName(extractUserQuery(firstUser.text)) : null;
75557
+ };
75558
+ return {
75559
+ cached(key) {
75560
+ return entries[key]?.title ?? null;
75561
+ },
75562
+ resolve(session) {
75563
+ const key = `${session.harness}/${session.id}`;
75564
+ const cached2 = entries[key];
75565
+ if (cached2 && (cached2.title !== null || cached2.mtime === mtime(session.path))) {
75566
+ return cached2.title;
75567
+ }
75568
+ const title = compute(session);
75569
+ entries[key] = { title, mtime: mtime(session.path) };
75570
+ dirty = true;
75571
+ return title;
75572
+ },
75573
+ flush() {
75574
+ if (!dirty)
75575
+ return;
75576
+ saveCacheFile2(configDir, entries);
75577
+ dirty = false;
75578
+ }
75579
+ };
75580
+ }
75581
+ var CACHE_FILENAME5 = "session-titles.json", CACHE_SCHEMA_VERSION2 = 3, HEAD_LINES2 = 50, SESSION_NAME_MAX = 80;
75582
+ var init_session_title = __esm(() => {
75583
+ init_config();
75584
+ init_read();
75585
+ });
75586
+
75398
75587
  // src/tui/alt-screen.ts
75399
75588
  function enterAltScreen(output2) {
75400
75589
  depth += 1;
@@ -75418,7 +75607,8 @@ var init_alt_screen = __esm(() => {
75418
75607
  });
75419
75608
 
75420
75609
  // src/tui/activity-view.ts
75421
- import { readFileSync as readFileSync24 } from "node:fs";
75610
+ import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
75611
+ import { basename as basename5 } from "node:path";
75422
75612
  function reduceActivityViewKey(key) {
75423
75613
  if (key === "q" || key === ESC7 || key === CTRL_C4)
75424
75614
  return "back";
@@ -75432,6 +75622,10 @@ function reduceActivityViewKey(key) {
75432
75622
  return "down";
75433
75623
  if (key === "s")
75434
75624
  return "sync";
75625
+ if (key === "f")
75626
+ return "full";
75627
+ if (key === "c")
75628
+ return "clear";
75435
75629
  return "none";
75436
75630
  }
75437
75631
  function cycleTab(tab, delta = 1) {
@@ -75454,33 +75648,49 @@ function syncConfirmMode(status) {
75454
75648
  return "resume";
75455
75649
  return "start";
75456
75650
  }
75457
- function formatActivityLine(line, width) {
75651
+ function formatActivityLine(line, width, full = false) {
75458
75652
  const compact = stripAnsiCodes(line).replace(/^\[(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})[^\]]*\]/, (_match, _date2, time3) => time3);
75459
- if (compact.length <= width)
75653
+ if (full || compact.length <= width)
75460
75654
  return compact;
75461
75655
  return `${compact.slice(0, Math.max(0, width - 1))}…`;
75462
75656
  }
75463
75657
  function appendSyncActivity(buffer, chunk, max = ACTIVITY_VIEW_BUFFER_LINES) {
75464
75658
  const fresh = chunk.split(`
75465
- `).filter((line) => line.includes("[sync]") || line.includes("[miner]")).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
75659
+ `).filter((line) => line.includes("[sync]") || line.includes("[learner]")).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
75466
75660
  return [...buffer, ...fresh].slice(-max);
75467
75661
  }
75468
75662
  function clip(text, max) {
75469
75663
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
75470
75664
  }
75471
- function formatMinedRow(record2) {
75665
+ function formatStudiedRow(record2, full = false, name) {
75472
75666
  const match = record2.at.match(/^\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2})/);
75473
75667
  const stamp = match ? `${match[1]} ${match[2]}` : record2.at;
75474
75668
  const slash = record2.session.indexOf("/");
75475
75669
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
75476
75670
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
75477
- const project = clip(record2.project ?? "-", 28);
75478
- return `${harness.padEnd(8)} ${stamp} ${project} ${clip(id2, 24)}`;
75671
+ const rawProject = record2.project ?? "-";
75672
+ const project = full ? rawProject : clip(rawProject, 28);
75673
+ const label = full ? name ? `${name} · ${id2}` : id2 : clip(name ?? id2, 44);
75674
+ return `${harness.padEnd(8)} ${stamp} ${project} ${label}`;
75479
75675
  }
75480
- function formatQueuedRow(session) {
75676
+ function formatQueuedRow(session, full = false, name) {
75481
75677
  const stamp = session.updated.replace("T", " ").slice(5, 16);
75482
- const project = clip(session.project ?? "-", 28);
75483
- return `${session.harness.padEnd(8)} ${stamp} ${project} ${clip(session.id, 24)}`;
75678
+ const rawProject = session.project ?? "-";
75679
+ const project = full ? rawProject : clip(rawProject, 28);
75680
+ const label = full ? name ? `${name} · ${session.id}` : session.id : clip(name ?? session.id, 44);
75681
+ return `${session.harness.padEnd(8)} ${stamp} ${project} ${label}`;
75682
+ }
75683
+ function wrapRow(text, width, indent = " ") {
75684
+ if (text.length <= width)
75685
+ return [text];
75686
+ const out = [text.slice(0, width)];
75687
+ let rest = text.slice(width);
75688
+ const inner = Math.max(1, width - indent.length);
75689
+ while (rest.length > 0) {
75690
+ out.push(indent + rest.slice(0, inner));
75691
+ rest = rest.slice(inner);
75692
+ }
75693
+ return out;
75484
75694
  }
75485
75695
  function windowList(lines, scroll, height = ACTIVITY_VIEW_LIST_LINES) {
75486
75696
  const clamped = Math.max(0, Math.min(scroll, Math.max(0, lines.length - height)));
@@ -75508,23 +75718,23 @@ function foldRunProgress(progress, chunk) {
75508
75718
  let current = progress;
75509
75719
  for (const line of chunk.split(`
75510
75720
  `)) {
75511
- const start = line.match(/\[sync\] mining (\d+) of \d+ ready sessions/);
75721
+ const start = line.match(/\[sync\] (?:studying|mining) (\d+) of \d+ ready sessions/);
75512
75722
  if (start) {
75513
75723
  current = { batch: Number.parseInt(start[1], 10), read: new Set, notes: 0 };
75514
75724
  continue;
75515
75725
  }
75516
- if (/\[sync\] (mined \d+ sessions|mining failed|mining skipped)/.test(line)) {
75726
+ if (/\[sync\] (?:studied|mined) \d+ sessions|\[sync\] (?:studying|mining) (?:failed|skipped)/.test(line)) {
75517
75727
  current = null;
75518
75728
  continue;
75519
75729
  }
75520
75730
  if (!current)
75521
75731
  continue;
75522
- const read = line.match(/\[miner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75732
+ const read = line.match(/\[learner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75523
75733
  if (read) {
75524
75734
  current.read.add(read[1]);
75525
75735
  continue;
75526
75736
  }
75527
- if (line.includes("[miner] [agent] → mcp__dosu__write_knowledge")) {
75737
+ if (line.includes("[learner] [agent] → mcp__dosu__write_knowledge")) {
75528
75738
  current.notes += 1;
75529
75739
  }
75530
75740
  }
@@ -75538,15 +75748,15 @@ function statusLine(status) {
75538
75748
  if (status.running) {
75539
75749
  const since = status.startedAt ? ` · since ${localTime(status.startedAt)}` : "";
75540
75750
  const pid = status.pid !== undefined ? ` (pid ${status.pid})` : "";
75541
- return `⛏️ ${import_picocolors30.default.bold(brand("Mining sessions..."))}${import_picocolors30.default.dim(`${pid}${since}`)}`;
75751
+ return `\uD83D\uDCDA ${import_picocolors30.default.bold(brand("Studying sessions..."))}${import_picocolors30.default.dim(`${pid}${since}`)}`;
75542
75752
  }
75543
75753
  if (status.state.paused) {
75544
- return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Paused")} ${import_picocolors30.default.dim("· mining stays off until you resume")}`;
75754
+ return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Paused")} ${import_picocolors30.default.dim("· studying stays off until you resume")}`;
75545
75755
  }
75546
75756
  if (status.staleLock) {
75547
75757
  return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Not running")} ${import_picocolors30.default.dim("· a previous run exited without cleaning up")}`;
75548
75758
  }
75549
- return `${import_picocolors30.default.dim("○")} ${import_picocolors30.default.bold("Idle")} ${import_picocolors30.default.dim("· hooks mine new sessions automatically")}`;
75759
+ return `${import_picocolors30.default.dim("○")} ${import_picocolors30.default.bold("Idle")} ${import_picocolors30.default.dim("· hooks study new sessions automatically")}`;
75550
75760
  }
75551
75761
  function progressLine(done, ready, width, notes = 0) {
75552
75762
  const total = done + ready;
@@ -75558,14 +75768,14 @@ function progressLine(done, ready, width, notes = 0) {
75558
75768
  const bar = brand("█".repeat(filled)) + import_picocolors30.default.dim("░".repeat(cells - filled));
75559
75769
  const pct = Math.floor(ratio * 100);
75560
75770
  const suffix = notes > 0 ? ` · ${notes} suggested page${notes === 1 ? "" : "s"}` : "";
75561
- return `${bar} ${done}/${total} mined · ${pct}%${suffix}`;
75771
+ return `${bar} ${done}/${total} studied · ${pct}%${suffix}`;
75562
75772
  }
75563
- function tabBar(tab, queuedCount, openCount, minedCount, width) {
75773
+ function tabBar(tab, queuedCount, openCount, studiedCount, width) {
75564
75774
  return tabStrip([
75565
- ["activity", "Activity"],
75566
- ["mined", `Mined (${minedCount})`],
75567
- ["queued", `Queued (${queuedCount})`],
75568
- ["open", `Open (${openCount})`]
75775
+ ["activity", "activity"],
75776
+ ["studied", `studied (${studiedCount})`],
75777
+ ["queued", `queued (${queuedCount})`],
75778
+ ["open", `open (${openCount})`]
75569
75779
  ], tab, width);
75570
75780
  }
75571
75781
  function activityWidth(columns) {
@@ -75587,10 +75797,11 @@ function wrapLine(text, width, indent = " ") {
75587
75797
  return out;
75588
75798
  }
75589
75799
  function confirmBox(queuedCount, backlog, width, mode = "start") {
75590
- const inFlight = backlog && backlog.inFlight > 0 ? ` (+${backlog.inFlight} open, mined once ${backlog.inFlight === 1 ? "it goes" : "they go"} quiet)` : "";
75591
- const scope = mode === "stop" ? "the run is killed mid-batch · mining stays paused until you resume" : queuedCount > 0 ? `${queuedCount} session${queuedCount === 1 ? "" : "s"} queued${inFlight} · runs in the background` : `queue empty${inFlight} · a run would only pick up sessions that finish from here`;
75592
- const title = mode === "stop" ? "Stop mining?" : mode === "resume" ? "Resume mining?" : "Start mining now?";
75593
- const verb = mode === "stop" ? "stop" : mode === "resume" ? "resume" : "start";
75800
+ const inFlight = backlog && backlog.inFlight > 0 ? ` (+${backlog.inFlight} still open; ${backlog.inFlight === 1 ? "it joins" : "they join"} once quiet)` : "";
75801
+ const sessions = `${queuedCount} session${queuedCount === 1 ? "" : "s"}`;
75802
+ const scope = mode === "clear" ? "Forgets which local sessions Dosu has already studied, so the next run reads them all again. Use this to rebuild knowledge from scratch. Notes already saved in Dosu are kept." : mode === "stop" ? "Kills the run mid-batch and pauses studying, so new sessions pile up in the queue instead of being read. Nothing is lost; press s again to resume from where it left off." : mode === "resume" ? `Studying picks up where it stopped: ${sessions} queued${inFlight}. Runs in the background; new notes appear in Dosu as each batch finishes.` : queuedCount > 0 ? `Dosu reads ${sessions}${inFlight} and distills the durable decisions and gotchas into team knowledge. Runs in the background; new notes appear in Dosu as each batch finishes.` : `Queue is empty${inFlight}. A run now would only pick up sessions that finish from here; hooks already do that automatically.`;
75803
+ const title = mode === "clear" ? "Clear study history?" : mode === "stop" ? "Stop studying?" : mode === "resume" ? "Resume studying?" : "Start studying now?";
75804
+ const verb = mode === "clear" ? "clear" : mode === "stop" ? "stop" : mode === "resume" ? "resume" : "start";
75594
75805
  const maxInner = Math.max(20, Math.min(width, contentWidth()) - 4);
75595
75806
  const rows = [
75596
75807
  import_picocolors30.default.bold(brand(title)),
@@ -75606,64 +75817,143 @@ function confirmBox(queuedCount, backlog, width, mode = "start") {
75606
75817
  import_picocolors30.default.dim(`╰${"─".repeat(inner + 2)}╯`)
75607
75818
  ];
75608
75819
  }
75609
- function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], minedBeforeRun = 0, open5 = [], runProgress = null) {
75610
- const mined = status.state.watermark ? `Mined sessions up to ${localTime(status.state.watermark)}` : "Nothing mined yet";
75820
+ function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], studiedBeforeRun = 0, open5 = [], runProgress = null, projectNames = {}, sessionNames = {}) {
75821
+ const studied = status.state.watermark ? `Studied sessions up to ${localTime(status.state.watermark)}` : "Nothing studied yet";
75611
75822
  const queueDetail = [];
75612
75823
  if (status.backoffUntil) {
75613
75824
  queueDetail.push(`retrying after ${localTime(status.backoffUntil)} · s syncs now`);
75614
75825
  }
75615
75826
  let progress = null;
75616
75827
  if (status.running && backlog) {
75617
- const minedDelta = Math.max(0, (status.state.total_mined ?? 0) - minedBeforeRun);
75828
+ const studiedDelta = Math.max(0, (status.state.total_mined ?? 0) - studiedBeforeRun);
75618
75829
  const stepDone = runProgress ? Math.min(Math.max(0, runProgress.read.size - 1), runProgress.batch - 1, backlog.ready) : 0;
75619
- progress = progressLine(minedDelta + stepDone, Math.max(0, backlog.ready - stepDone), width, runProgress?.notes ?? 0);
75830
+ progress = progressLine(studiedDelta + stepDone, Math.max(0, backlog.ready - stepDone), width, runProgress?.notes ?? 0);
75620
75831
  }
75621
75832
  const refusal = !status.running && status.state.last_refusal;
75622
- const refusalLines = refusal ? wrapLine(`! Mining paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75623
- const minedRows = (status.state.mined_sessions ?? []).map((record2) => formatActivityLine(formatMinedRow(record2), width));
75624
- const queuedRows = queued.map((session) => formatActivityLine(formatQueuedRow(session), width));
75625
- const openRows = open5.map((session) => formatActivityLine(formatQueuedRow(session), width));
75626
- const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : minedRows;
75627
- const emptyMined = status.state.watermark ? "No sessions recorded yet. History starts with the next mining run." : "No mined sessions yet.";
75628
- const empty = pane.tab === "activity" ? "No sync activity in the log yet." : pane.tab === "queued" ? "Queue empty. Finished agent sessions appear here." : pane.tab === "open" ? "No open sessions. Live agent sessions sit here until they go quiet." : emptyMined;
75629
- const { visible, above, below } = windowList(source, pane.scroll);
75630
- const listRows = visible.length > 0 ? visible.map((row) => import_picocolors30.default.dim(row)) : [import_picocolors30.default.dim(empty)];
75833
+ const refusalLines = refusal ? wrapLine(`! Studying paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75834
+ const fullRows = Boolean(pane.fullRows);
75835
+ const withName = (item, key) => projectNames[key] ? { ...item, project: projectNames[key] } : item;
75836
+ const latestPass = new Map;
75837
+ for (const r9 of status.state.mined_sessions ?? [])
75838
+ latestPass.set(r9.session, r9);
75839
+ const studiedRows = [...latestPass.values()].map((r9) => {
75840
+ const record2 = withName(r9, r9.session);
75841
+ const name = sessionNames[r9.session];
75842
+ return fullRows ? formatStudiedRow(record2, true, name) : formatActivityLine(formatStudiedRow(record2, false, name), width);
75843
+ });
75844
+ const sessionRow = (session) => {
75845
+ const key = `${session.harness}/${session.id}`;
75846
+ const s = withName(session, key);
75847
+ const name = sessionNames[key];
75848
+ return fullRows ? formatQueuedRow(s, true, name) : formatActivityLine(formatQueuedRow(s, false, name), width);
75849
+ };
75850
+ const queuedRows = queued.map(sessionRow);
75851
+ const openRows = open5.map(sessionRow);
75852
+ const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width, fullRows)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : studiedRows;
75853
+ const emptyStudied = status.state.watermark ? "No sessions recorded yet. History starts with the next study run." : "No studied sessions yet.";
75854
+ const empty = pane.tab === "activity" ? "No sync activity in the log yet." : pane.tab === "queued" ? "Queue empty. Finished agent sessions appear here." : pane.tab === "open" ? "No open sessions. Live agent sessions sit here until they go quiet." : emptyStudied;
75855
+ const height = fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75856
+ const { visible, above, below } = windowList(source, pane.scroll, height);
75857
+ const display = fullRows ? visible.flatMap((row) => wrapRow(row, width)) : visible;
75858
+ const listRows = display.length > 0 ? display.map((row) => import_picocolors30.default.dim(row)) : [import_picocolors30.default.dim(empty)];
75631
75859
  const scrollParts = [];
75632
75860
  if (above > 0)
75633
75861
  scrollParts.push(`↑ ${above} earlier`);
75634
75862
  if (below > 0)
75635
75863
  scrollParts.push(`↓ ${below} newer`);
75636
- if (pane.tab !== "activity" && visible.length > 0) {
75637
- scrollParts.push(`full rows: dosu knowledge sessions --${pane.tab}`);
75638
- }
75639
75864
  const lines = [
75640
- breadcrumb(["Home", "Activity"], width),
75865
+ breadcrumb(["home", "activity"], width),
75641
75866
  "",
75642
75867
  statusLine(status),
75643
- import_picocolors30.default.dim(mined),
75868
+ import_picocolors30.default.dim(studied),
75644
75869
  ...queueDetail.length > 0 ? wrapLine(queueDetail.join(" · "), width).map((line) => import_picocolors30.default.dim(line)) : [],
75645
75870
  ...progress ? [progress] : [],
75646
75871
  ...refusalLines,
75647
75872
  "",
75648
- ...tabBar(pane.tab, queued.length, open5.length, status.state.total_mined ?? minedRows.length, width),
75873
+ ...tabBar(pane.tab, queued.length, open5.length, latestPass.size, width),
75649
75874
  ...listRows,
75650
- ...scrollParts.length > 0 ? [import_picocolors30.default.dim(scrollParts.join(" · "))] : [],
75875
+ ...scrollParts.length > 0 ? [import_picocolors30.default.dim(clip(scrollParts.join(" · "), width))] : [],
75651
75876
  "",
75652
75877
  ...pane.confirm ? centerBlock(confirmBox(queued.length, backlog, width, pane.confirm), width) : [
75653
- import_picocolors30.default.dim([
75878
+ ...wrapLine([
75654
75879
  "tab switch",
75655
75880
  "↑↓ scroll",
75881
+ fullRows ? "f clip" : "f full rows",
75656
75882
  status.running ? "s stop" : status.state.paused ? "s resume" : "s sync now",
75883
+ ...!status.running && status.state.watermark ? ["c clear"] : [],
75657
75884
  "esc back"
75658
- ].join(" · "))
75885
+ ].join(" · "), width, "").map((line) => import_picocolors30.default.dim(line))
75659
75886
  ]
75660
75887
  ];
75661
75888
  return lines.join(`
75662
75889
  `);
75663
75890
  }
75891
+ function createRowNamer() {
75892
+ const dirs = createProjectDirResolver();
75893
+ const titles = createSessionTitleResolver();
75894
+ const bySlug = new Map;
75895
+ const fromSlug = (slug) => {
75896
+ if (!bySlug.has(slug))
75897
+ bySlug.set(slug, unmungeSlug(slug));
75898
+ return bySlug.get(slug) ?? null;
75899
+ };
75900
+ return (status, backlog) => {
75901
+ const names = { projects: {}, titles: {} };
75902
+ let budget = TITLE_READS_PER_DRAW;
75903
+ let resolved = false;
75904
+ const title = (key, session) => {
75905
+ const hit = titles.cached(key);
75906
+ if (hit) {
75907
+ names.titles[key] = hit;
75908
+ return;
75909
+ }
75910
+ if (budget <= 0)
75911
+ return;
75912
+ budget--;
75913
+ const t9 = titles.resolve(session);
75914
+ resolved = true;
75915
+ if (t9)
75916
+ names.titles[key] = t9;
75917
+ };
75918
+ for (const s of [...backlog.queued, ...backlog.open]) {
75919
+ if (!existsSync26(s.path))
75920
+ continue;
75921
+ const key = `${s.harness}/${s.id}`;
75922
+ const dir = dirs.resolve(s);
75923
+ if (dir)
75924
+ names.projects[key] = basename5(dir);
75925
+ title(key, s);
75926
+ resolved = true;
75927
+ }
75928
+ for (const r9 of [...status.state.mined_sessions ?? []].reverse()) {
75929
+ const key = r9.session;
75930
+ if (!names.projects[key]) {
75931
+ const dir = dirs.cached(key) ?? (r9.project ? fromSlug(r9.project) : null);
75932
+ if (dir)
75933
+ names.projects[key] = basename5(dir);
75934
+ }
75935
+ if (!names.titles[key]) {
75936
+ const hit = titles.cached(key);
75937
+ if (hit) {
75938
+ names.titles[key] = hit;
75939
+ } else if (budget > 0) {
75940
+ const slash = key.indexOf("/");
75941
+ const session = reconstructSession(key.slice(0, slash), key.slice(slash + 1), r9.project);
75942
+ if (session)
75943
+ title(key, session);
75944
+ }
75945
+ }
75946
+ }
75947
+ if (resolved) {
75948
+ dirs.flush();
75949
+ titles.flush();
75950
+ }
75951
+ return names;
75952
+ };
75953
+ }
75664
75954
  function defaultReadLog2() {
75665
75955
  try {
75666
- return readFileSync24(logger.getLogPath(), "utf-8");
75956
+ return readFileSync25(logger.getLogPath(), "utf-8");
75667
75957
  } catch {
75668
75958
  return "";
75669
75959
  }
@@ -75679,7 +75969,9 @@ function runActivityView(io2 = {}) {
75679
75969
  const startSync = io2.startSync ?? (() => spawnDetachedSelf(["knowledge", "sync", "--bootstrap"]));
75680
75970
  const stopSync = io2.stopSync ?? stopSyncRun;
75681
75971
  const setPaused = io2.setPaused ?? setSyncPaused;
75972
+ const clearHistory = io2.clearHistory ?? resetSyncState;
75682
75973
  const listBacklog = io2.listBacklog ?? listSessionBacklog;
75974
+ const rowNamesFor = io2.rowNames ?? createRowNamer();
75683
75975
  const pollMs = io2.pollMs ?? ACTIVITY_VIEW_POLL_MS;
75684
75976
  const seed = readLog();
75685
75977
  let activity = appendSyncActivity([], seed);
@@ -75687,6 +75979,7 @@ function runActivityView(io2 = {}) {
75687
75979
  let runProgress = foldRunProgress(null, seed);
75688
75980
  let tab = "activity";
75689
75981
  let scroll = 0;
75982
+ let fullRows = false;
75690
75983
  let confirmSync = null;
75691
75984
  let status = getStatus();
75692
75985
  let sessions = listBacklog();
@@ -75705,30 +75998,32 @@ function runActivityView(io2 = {}) {
75705
75998
  return sessions.open.length;
75706
75999
  return (status.state.mined_sessions ?? []).length;
75707
76000
  };
76001
+ const listHeight = () => fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75708
76002
  let lastFrame = null;
75709
- let minedBeforeRun = null;
76003
+ let studiedBeforeRun = null;
75710
76004
  const draw = () => {
75711
76005
  status = getStatus();
75712
76006
  if (status.running) {
75713
76007
  const run2 = status.state.run;
75714
76008
  if (run2 && run2.pid === status.pid) {
75715
- minedBeforeRun = run2.baseline_mined;
76009
+ studiedBeforeRun = run2.baseline_mined;
75716
76010
  } else {
75717
- minedBeforeRun ??= status.state.total_mined ?? 0;
76011
+ studiedBeforeRun ??= status.state.total_mined ?? 0;
75718
76012
  }
75719
76013
  } else {
75720
- minedBeforeRun = null;
76014
+ studiedBeforeRun = null;
75721
76015
  }
75722
76016
  if (status.state.watermark !== queuedWatermark) {
75723
76017
  queuedWatermark = status.state.watermark;
75724
76018
  sessions = listBacklog();
75725
76019
  }
75726
- if ((confirmSync === "start" || confirmSync === "resume") && status.running)
76020
+ if (confirmSync !== null && confirmSync !== "stop" && status.running)
75727
76021
  confirmSync = null;
75728
76022
  if (confirmSync === "stop" && !status.running)
75729
76023
  confirmSync = null;
75730
76024
  const width = activityWidth(output2.columns ?? 80);
75731
- const frame = renderActivityFrame(status, activity, width, backlog, { tab, scroll, confirm: confirmSync ?? undefined }, sessions.queued, minedBeforeRun ?? 0, sessions.open, runProgress);
76025
+ const rowNames = rowNamesFor(status, sessions);
76026
+ const frame = renderActivityFrame(status, activity, width, backlog, { tab, scroll, confirm: confirmSync ?? undefined, fullRows }, sessions.queued, studiedBeforeRun ?? 0, sessions.open, runProgress, rowNames.projects, rowNames.titles);
75732
76027
  if (frame === lastFrame)
75733
76028
  return;
75734
76029
  lastFrame = frame;
@@ -75775,11 +76070,15 @@ ${CLEAR_BELOW}`);
75775
76070
  const mode = confirmSync;
75776
76071
  confirmSync = null;
75777
76072
  let note;
75778
- if (mode === "stop") {
76073
+ if (mode === "clear") {
76074
+ clearHistory();
76075
+ studiedBeforeRun = null;
76076
+ note = "[sync] study history cleared · the next run reads every local session again";
76077
+ } else if (mode === "stop") {
75779
76078
  const ok2 = status.pid !== undefined && stopSync(status.pid);
75780
76079
  if (ok2)
75781
76080
  setPaused(true);
75782
- note = ok2 ? "[sync] mining stopped · paused until you resume" : "[sync] could not stop the run · it may have just finished";
76081
+ note = ok2 ? "[sync] studying stopped · paused until you resume" : "[sync] could not stop the run · it may have just finished";
75783
76082
  } else {
75784
76083
  if (mode === "resume")
75785
76084
  setPaused(false);
@@ -75803,14 +76102,23 @@ ${CLEAR_BELOW}`);
75803
76102
  if (action === "sync") {
75804
76103
  confirmSync = syncConfirmMode(status);
75805
76104
  draw();
76105
+ } else if (action === "clear") {
76106
+ if (!status.running && status.state.watermark) {
76107
+ confirmSync = "clear";
76108
+ draw();
76109
+ }
75806
76110
  } else if (action === "tab" || action === "tab-back") {
75807
76111
  tab = cycleTab(tab, action === "tab" ? 1 : -1);
75808
76112
  scroll = 0;
75809
76113
  if (tab === "queued" || tab === "open")
75810
76114
  sessions = listBacklog();
75811
76115
  draw();
76116
+ } else if (action === "full") {
76117
+ fullRows = !fullRows;
76118
+ scroll = 0;
76119
+ draw();
75812
76120
  } else if (action === "up") {
75813
- const maxScroll = Math.max(0, activeListLength() - ACTIVITY_VIEW_LIST_LINES);
76121
+ const maxScroll = Math.max(0, activeListLength() - listHeight());
75814
76122
  if (scroll < maxScroll) {
75815
76123
  scroll += 1;
75816
76124
  draw();
@@ -75826,10 +76134,12 @@ ${CLEAR_BELOW}`);
75826
76134
  input2.on("data", onData);
75827
76135
  });
75828
76136
  }
75829
- var import_picocolors30, ESC7, HIDE_CURSOR3, SHOW_CURSOR3, CTRL_C4, KEY_UP4, KEY_DOWN4, KEY_RIGHT3, KEY_LEFT3, CURSOR_HOME, CLEAR_BELOW, CLEAR_EOL, ACTIVITY_VIEW_POLL_MS = 500, ACTIVITY_VIEW_LIST_LINES = 10, ACTIVITY_VIEW_BUFFER_LINES = 200, ACTIVITY_VIEW_TABS, DEFAULT_PANE;
76137
+ var import_picocolors30, ESC7, HIDE_CURSOR3, SHOW_CURSOR3, CTRL_C4, KEY_UP4, KEY_DOWN4, KEY_RIGHT3, KEY_LEFT3, CURSOR_HOME, CLEAR_BELOW, CLEAR_EOL, ACTIVITY_VIEW_POLL_MS = 500, ACTIVITY_VIEW_LIST_LINES = 10, ACTIVITY_VIEW_FULL_LIST_ROWS = 5, ACTIVITY_VIEW_BUFFER_LINES = 200, ACTIVITY_VIEW_TABS, DEFAULT_PANE, TITLE_READS_PER_DRAW = 25;
75830
76138
  var init_activity_view = __esm(() => {
75831
76139
  init_follow();
75832
76140
  init_logger();
76141
+ init_project_dir();
76142
+ init_session_title();
75833
76143
  init_styles();
75834
76144
  init_backlog();
75835
76145
  init_detach();
@@ -75851,7 +76161,7 @@ var init_activity_view = __esm(() => {
75851
76161
  CURSOR_HOME = `${ESC7}[H`;
75852
76162
  CLEAR_BELOW = `${ESC7}[0J`;
75853
76163
  CLEAR_EOL = `${ESC7}[K`;
75854
- ACTIVITY_VIEW_TABS = ["activity", "mined", "queued", "open"];
76164
+ ACTIVITY_VIEW_TABS = ["activity", "studied", "queued", "open"];
75855
76165
  DEFAULT_PANE = { tab: "activity", scroll: 0 };
75856
76166
  });
75857
76167
 
@@ -75864,7 +76174,7 @@ async function trackCliOnboardingEvent(cfg, onboardingRunID, event, properties =
75864
76174
  return;
75865
76175
  if (!cfg.active_account?.session.access_token)
75866
76176
  return;
75867
- if (!isUUID2(onboardingRunID))
76177
+ if (!isUUID3(onboardingRunID))
75868
76178
  return;
75869
76179
  const input2 = {
75870
76180
  event,
@@ -75887,7 +76197,7 @@ async function trackCliOnboardingPreAuthEvent(onboardingRunID, event, properties
75887
76197
  return;
75888
76198
  if (!analyticsReleaseEnabled())
75889
76199
  return;
75890
- if (!isUUID2(onboardingRunID))
76200
+ if (!isUUID3(onboardingRunID))
75891
76201
  return;
75892
76202
  const input2 = {
75893
76203
  event,
@@ -75920,13 +76230,14 @@ function analyticsReleaseEnabled() {
75920
76230
  return Boolean(parsePostHogProjectToken(process.env.DOSU_POSTHOG_PROJECT_TOKEN_OVERRIDE ?? "phc_Uo9RxhN0HRk8gZkPlT18BCr7eqsM0MPWVW88HZB95V5"));
75921
76231
  }
75922
76232
  function baseProperties(cfg) {
76233
+ const orgId = cfg.active_account?.target?.org_id;
75923
76234
  return {
75924
76235
  cli_version: safeIdentifier(VERSION, 32),
75925
76236
  install_channel: safeIdentifier(INSTALL_CHANNEL, 32),
75926
76237
  platform: safeIdentifier(process.platform, 24),
75927
76238
  arch: safeIdentifier(process.arch, 24),
75928
76239
  mode: cfg.mode ?? "cloud",
75929
- org_id: safeIdentifier(cfg.active_account?.target?.org_id, 128),
76240
+ ...orgId && isUUID3(orgId) ? { org_id: orgId, $groups: { organization: orgId } } : {},
75930
76241
  deployment_id: safeIdentifier(cfg.active_account?.target?.deployment_id, 128),
75931
76242
  space_id: safeIdentifier(cfg.active_account?.target?.space_id, 128)
75932
76243
  };
@@ -75961,7 +76272,7 @@ function allowlistedWorkflowProperties(properties) {
75961
76272
  function safeIdentifier(value, maxLength) {
75962
76273
  return typeof value === "string" && value.length <= maxLength && /^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(value) ? value : undefined;
75963
76274
  }
75964
- function isUUID2(value) {
76275
+ function isUUID3(value) {
75965
76276
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
75966
76277
  }
75967
76278
  function createAnalyticsClient(accessToken, requestSignal) {
@@ -76684,23 +76995,31 @@ async function stepOfferInitialSync(cfg) {
76684
76995
  s.start("Checking for recent agent sessions...");
76685
76996
  const outcome = await runKnowledgeSync({ bootstrap: true });
76686
76997
  if (outcome.status !== "backlog" || outcome.readySessions === 0) {
76687
- s.stop("No unmined agent sessions found. Dosu will learn as you work.");
76998
+ s.stop("No past agent sessions to study. Dosu will learn from new ones as you work.");
76688
76999
  return;
76689
77000
  }
76690
77001
  const n5 = outcome.readySessions;
76691
- s.stop(`Found ${n5} unmined agent session${n5 === 1 ? "" : "s"} on this machine.`);
77002
+ const them = n5 === 1 ? "it" : "them";
77003
+ s.stop(`Found ${n5} past agent session${n5 === 1 ? "" : "s"} on this machine that Dosu hasn't studied.`);
77004
+ log2.message([
77005
+ `Dosu can read ${them} and pull out the durable knowledge: decisions, gotchas, and`,
77006
+ "how-things-work that your team and agents would otherwise rediscover.",
77007
+ "Only distilled notes are saved to Dosu; your session logs stay on this machine.",
77008
+ "It runs in the background a few sessions at a time; notes appear in Dosu as each batch finishes."
77009
+ ].join(`
77010
+ `));
76692
77011
  const mineNow = await confirm2({
76693
- message: `Mine ${n5 === 1 ? "it" : "them"} for team knowledge now? (runs in the background)`,
76694
- active: "Mine now ⛏️",
76695
- inactive: "Skip",
77012
+ message: `Study ${them} now?`,
77013
+ active: "Study now \uD83D\uDCDA",
77014
+ inactive: "Skip for now",
76696
77015
  initialValue: true
76697
77016
  });
76698
77017
  if (isCancel2(mineNow) || !mineNow) {
76699
- log2.info(`Skipped. Dosu picks sessions up in the background as you work, or run ${info("dosu knowledge sync")} anytime.`);
77018
+ log2.info(`Skipped. Dosu studies new sessions in the background as you work; run ${info("dosu knowledge sync")} anytime to study these too.`);
76700
77019
  return;
76701
77020
  }
76702
77021
  if (spawnDetachedSelf(["knowledge", "sync", "--quiet", "--bootstrap"])) {
76703
- log2.success("⛏️ Currently mining... Dosu is distilling your past sessions into team knowledge, a few at a time in the background.");
77022
+ log2.success(`\uD83D\uDCDA Studying ${n5} session${n5 === 1 ? "" : "s"} in the background. Watch progress on the Activity screen; when it finishes, the new notes are in Dosu and agents connected to this MCP can use them.`);
76704
77023
  const watch = await confirm2({
76705
77024
  message: "What next?",
76706
77025
  active: "Watch it work",
@@ -77525,12 +77844,15 @@ function loadPageStatsFromConfig() {
77525
77844
  }
77526
77845
  }
77527
77846
  function overviewRows(state) {
77528
- const minedTotal = state.total_mined ?? 0;
77847
+ const studiedTotal = state.total_mined ?? 0;
77529
77848
  const notes = state.total_notes ?? 0;
77530
77849
  const tokens = state.total_learning_tokens ?? 0;
77531
- if (minedTotal === 0 && notes === 0)
77850
+ if (studiedTotal === 0 && notes === 0)
77532
77851
  return [];
77533
- const rows = [`${label("Sessions mined")}${minedTotal}`, `${label("Suggested pages")}${notes}`];
77852
+ const rows = [
77853
+ `${label("Sessions studied")}${studiedTotal}`,
77854
+ `${label("Suggested pages")}${notes}`
77855
+ ];
77534
77856
  if (tokens > 0) {
77535
77857
  rows.push(`${label("Investigation distilled")}${formatTokenCount(tokens)} tokens`);
77536
77858
  }
@@ -77579,7 +77901,7 @@ function analyticsTabRows(tab, state, pageStats) {
77579
77901
  function renderAnalyticsFrame(state, tab, scroll, pageStats = null, pagesPending = false, width = contentWidth()) {
77580
77902
  const rows = analyticsTabRows(tab, state, pageStats);
77581
77903
  const { visible, above, below } = windowReport(rows, scroll);
77582
- const empty = tab === "overview" ? "No analytics yet. They appear after the first mining run." : tab === "projects" ? "No per-project history yet. It fills in as sessions are mined." : pagesPending ? "Loading page analytics..." : "No page analytics yet.";
77904
+ const empty = tab === "overview" ? "No analytics yet. They appear after the first study run." : tab === "projects" ? "No per-project history yet. It fills in as sessions are studied." : pagesPending ? "Loading page analytics..." : "No page analytics yet.";
77583
77905
  const listRows = visible.length > 0 ? visible.map((row) => import_picocolors32.default.dim(row)) : [import_picocolors32.default.dim(empty)];
77584
77906
  const scrollParts = [];
77585
77907
  if (above > 0)
@@ -77587,12 +77909,12 @@ function renderAnalyticsFrame(state, tab, scroll, pageStats = null, pagesPending
77587
77909
  if (below > 0)
77588
77910
  scrollParts.push(`↓ ${below} more`);
77589
77911
  const lines = [
77590
- breadcrumb(["Home", "Analytics"], width),
77912
+ breadcrumb(["home", "analytics"], width),
77591
77913
  "",
77592
77914
  ...tabStrip([
77593
- ["overview", "Overview"],
77594
- ["projects", "Projects"],
77595
- ["pages", "Pages"]
77915
+ ["overview", "overview"],
77916
+ ["projects", "projects"],
77917
+ ["pages", "pages"]
77596
77918
  ], tab, width, { spread: false }),
77597
77919
  "",
77598
77920
  ...listRows,
@@ -77731,7 +78053,7 @@ function checklistRows(ctx) {
77731
78053
  const rows = [["workspace", ctx.directory]];
77732
78054
  rows.push([
77733
78055
  "account",
77734
- ctx.signedIn ? `${on2} signed in` : `${off} ${import_picocolors33.default.dim("not signed in · run Setup")}`
78056
+ ctx.sessionExpired ? `${off} ${import_picocolors33.default.yellow("session expired")} ${import_picocolors33.default.dim(`${DOT3} run Log in`)}` : ctx.signedIn ? `${on2} signed in` : `${off} ${import_picocolors33.default.dim(`not signed in ${DOT3} run Setup`)}`
77735
78057
  ]);
77736
78058
  const missing = new Set(ctx.setupMissing ?? []);
77737
78059
  const warnRow = `${off} ${import_picocolors33.default.yellow("not configured")} ${import_picocolors33.default.dim(`${DOT3} run Setup`)}`;
@@ -77755,10 +78077,10 @@ function checklistRows(ctx) {
77755
78077
  rows.push(["agents", `${on2} ${ctx.agents.join(` ${DOT3} `)}`]);
77756
78078
  if (missing.has("hooks"))
77757
78079
  rows.push(["hooks", warnRow]);
77758
- if (ctx.mining) {
78080
+ if (ctx.studying) {
77759
78081
  rows.push([
77760
78082
  "sync",
77761
- `⛏️ ${brand("mining sessions...")} ${import_picocolors33.default.dim(`${DOT3} see Activity`)}`
78083
+ `\uD83D\uDCDA ${brand("studying sessions...")} ${import_picocolors33.default.dim(`${DOT3} see activity`)}`
77762
78084
  ]);
77763
78085
  }
77764
78086
  if (ctx.update) {
@@ -77935,7 +78257,7 @@ function renderPagesFrame(model, width, selected) {
77935
78257
  }
77936
78258
  const count = model.phase === "ready" ? ` ${import_picocolors34.default.dim(model.pages.length < model.total ? `(${model.pages.length} of ${model.total})` : `(${model.pages.length})`)}` : "";
77937
78259
  const lines = [
77938
- `${breadcrumb(["Home", "Pages"], width)}${count}`,
78260
+ `${breadcrumb(["home", "pages"], width)}${count}`,
77939
78261
  "",
77940
78262
  ...body,
77941
78263
  ...scrollHint ? [import_picocolors34.default.dim(scrollHint)] : [],
@@ -77965,7 +78287,7 @@ function renderPageFrame(page, content, width, scroll) {
77965
78287
  }
77966
78288
  const meta3 = `${page.type} · ${page.published ? "published" : "draft"} · updated ${page.updated_at.slice(0, 10)}`;
77967
78289
  const lines = [
77968
- breadcrumb(["Home", "Pages", page.title], width),
78290
+ breadcrumb(["home", "pages", page.title], width),
77969
78291
  import_picocolors34.default.dim(clip3(meta3, width)),
77970
78292
  "",
77971
78293
  ...body,
@@ -78148,9 +78470,19 @@ __export(exports_tui, {
78148
78470
  runTUI: () => runTUI,
78149
78471
  handleLogout: () => handleLogout
78150
78472
  });
78151
- import { homedir as homedir10 } from "node:os";
78152
- import { basename as basename4 } from "node:path";
78153
- function bannerContext(cfg) {
78473
+ import { homedir as homedir12 } from "node:os";
78474
+ import { basename as basename6 } from "node:path";
78475
+ async function probeSession(cfg) {
78476
+ if (!isAuthenticated(cfg) || !isTokenExpired(cfg))
78477
+ return { expired: false };
78478
+ try {
78479
+ await new Client(cfg).refreshToken();
78480
+ return { expired: false };
78481
+ } catch (err) {
78482
+ return { expired: err instanceof SessionExpiredError };
78483
+ }
78484
+ }
78485
+ function bannerContext(cfg, session) {
78154
78486
  let webAppHost = "app.dosu.dev";
78155
78487
  try {
78156
78488
  webAppHost = new URL(getWebAppURL()).host;
@@ -78166,14 +78498,15 @@ function bannerContext(cfg) {
78166
78498
  return {
78167
78499
  version: getVersionString(),
78168
78500
  webAppHost,
78169
- directory: basename4(process.cwd()),
78501
+ directory: basename6(process.cwd()),
78170
78502
  signedIn: isAuthenticated(cfg),
78503
+ sessionExpired: session.expired,
78171
78504
  deploymentName: cfg.active_account?.target?.deployment_name,
78172
78505
  libraryName: cfg.active_account?.target?.library_name,
78173
- setupMissing: isAuthenticated(cfg) ? missingSetupSteps(cfg) : [],
78506
+ setupMissing: isAuthenticated(cfg) && !session.expired ? missingSetupSteps(cfg) : [],
78174
78507
  ...inGitWorkTree() ? { repoAgentsMd: dosuAgentsSectionState() } : {},
78175
78508
  agents,
78176
- mining: isMining(),
78509
+ studying: isStudying(),
78177
78510
  ...latest ? { update: { version: latest, hint: buildUpdateHint(INSTALL_CHANNEL, isNpxInvocation()) } } : {}
78178
78511
  };
78179
78512
  }
@@ -78240,24 +78573,25 @@ function setupHint(cfg) {
78240
78573
  const started = Boolean(cfg.active_account?.target);
78241
78574
  return import_picocolors35.default.yellow(started ? `incomplete · missing ${missingSetupSteps(cfg).join(" + ")}` : "not set up yet");
78242
78575
  }
78243
- function isMining() {
78576
+ function isStudying() {
78244
78577
  try {
78245
78578
  return getSyncStatus({ readLog: () => "" }).running;
78246
78579
  } catch {
78247
78580
  return false;
78248
78581
  }
78249
78582
  }
78250
- function drawHome(cfg) {
78583
+ function drawHome(cfg, session) {
78251
78584
  if (process.stdout.isTTY) {
78252
78585
  process.stdout.write(CLEAR_SCREEN);
78253
78586
  process.stdout.write(`
78254
78587
  `.repeat(frameTopMargin()));
78255
78588
  }
78256
- process.stdout.write(`${renderBanner(bannerContext(cfg))}
78589
+ process.stdout.write(`${renderBanner(bannerContext(cfg, session))}
78257
78590
  `);
78258
78591
  }
78259
78592
  async function runMainMenu() {
78260
78593
  const cfg = loadConfig();
78594
+ const session = await probeSession(cfg);
78261
78595
  const runSetupAndReload = async () => {
78262
78596
  await runSetup();
78263
78597
  const fresh = loadConfig();
@@ -78265,34 +78599,44 @@ async function runMainMenu() {
78265
78599
  cfg.active_account = fresh.active_account;
78266
78600
  };
78267
78601
  const buildOptions = () => {
78602
+ if (session.expired) {
78603
+ return [
78604
+ {
78605
+ label: "log in again",
78606
+ hint: "(session expired · opens your browser)",
78607
+ value: "auth"
78608
+ },
78609
+ { label: "exit", value: "exit" }
78610
+ ];
78611
+ }
78268
78612
  if (!isAuthenticated(cfg)) {
78269
78613
  return [
78270
- { label: "Log in / Sign up", hint: "(opens your browser)", value: "auth" },
78271
- { label: "Exit", value: "exit" }
78614
+ { label: "log in / sign up", hint: "(opens your browser)", value: "auth" },
78615
+ { label: "exit", value: "exit" }
78272
78616
  ];
78273
78617
  }
78274
78618
  if (!isSetUp(cfg)) {
78275
78619
  return [
78276
- { label: "Setup", hint: setupHint(cfg), value: "setup" },
78277
- { label: "Exit", value: "exit" }
78620
+ { label: "setup", hint: setupHint(cfg), value: "setup" },
78621
+ { label: "exit", value: "exit" }
78278
78622
  ];
78279
78623
  }
78280
- const mining = isMining();
78624
+ const studying = isStudying();
78281
78625
  return [
78282
78626
  {
78283
- label: mining ? `Activity ⛏️ ${brand("mining sessions...")}` : "Activity",
78627
+ label: studying ? `activity \uD83D\uDCDA ${brand("studying sessions...")}` : "activity",
78284
78628
  value: "sync"
78285
78629
  },
78286
- { label: "Knowledge report", hint: "(opens in browser)", value: "report" },
78287
- { label: "Analytics", value: "analytics" },
78288
- { label: "Pages", value: "pages" },
78289
- { label: "Settings", value: "settings" },
78290
- { label: "Exit", value: "exit" }
78630
+ { label: "knowledge report", hint: "(opens in browser)", value: "report" },
78631
+ { label: "analytics", value: "analytics" },
78632
+ { label: "pages", value: "pages" },
78633
+ { label: "settings", value: "settings" },
78634
+ { label: "exit", value: "exit" }
78291
78635
  ];
78292
78636
  };
78293
- const home = () => drawHome(cfg);
78637
+ const home = () => drawHome(cfg, session);
78294
78638
  home();
78295
- if (isAuthenticated(cfg) && !isSetUp(cfg)) {
78639
+ if (isAuthenticated(cfg) && !session.expired && !isSetUp(cfg)) {
78296
78640
  await runSetupAndReload();
78297
78641
  home();
78298
78642
  }
@@ -78326,6 +78670,7 @@ async function runMainMenu() {
78326
78670
  break;
78327
78671
  case "auth":
78328
78672
  await handleAuthenticate(cfg);
78673
+ session.expired = session.expired && isTokenExpired(cfg);
78329
78674
  home();
78330
78675
  if (isAuthenticated(cfg) && !isSetUp(cfg)) {
78331
78676
  await runSetupAndReload();
@@ -78355,19 +78700,19 @@ async function runSettings(cfg) {
78355
78700
  const target = cfg.active_account?.target;
78356
78701
  const library = target?.library_name ?? target?.deployment_name ?? "not configured";
78357
78702
  const filter = loadSyncState().project_filter;
78358
- const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename4(dir)).join(", ") : `${filter.length} projects`;
78359
- const action = await menuSelect("Settings", [
78360
- { label: "Switch organization", hint: target?.org_name, value: "switch-org" },
78361
- { label: "Switch Library", hint: library, value: "switch-library" },
78362
- { label: "Mining scope", hint: scope, value: "projects" },
78363
- { label: "Run setup", hint: "rerun the setup wizard", value: "setup" },
78364
- { label: "Log out", hint: "clear stored credentials", value: "logout" },
78365
- { label: "Back", value: "back" }
78703
+ const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename6(dir)).join(", ") : `${filter.length} projects`;
78704
+ const action = await menuSelect("settings", [
78705
+ { label: "switch organization", hint: target?.org_name, value: "switch-org" },
78706
+ { label: "switch library", hint: library, value: "switch-library" },
78707
+ { label: "study scope", hint: scope, value: "projects" },
78708
+ { label: "run setup", hint: "rerun the setup wizard", value: "setup" },
78709
+ { label: "log out", hint: "clear stored credentials", value: "logout" },
78710
+ { label: "back", value: "back" }
78366
78711
  ]);
78367
78712
  if (action === null || action === "back")
78368
78713
  return;
78369
78714
  if (action === "projects") {
78370
- await runMiningProjectsSetting();
78715
+ await runStudyingProjectsSetting();
78371
78716
  continue;
78372
78717
  }
78373
78718
  if (action === "setup") {
@@ -78390,7 +78735,7 @@ async function runSettings(cfg) {
78390
78735
  }
78391
78736
  }
78392
78737
  function displayDir(dir) {
78393
- const home = homedir10();
78738
+ const home = homedir12();
78394
78739
  return dir === home ? "~" : dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir;
78395
78740
  }
78396
78741
  function discoverProjectDirs() {
@@ -78405,7 +78750,7 @@ function discoverProjectDirs() {
78405
78750
  } catch {}
78406
78751
  return [...counts.entries()].sort((a9, b2) => b2[1] - a9[1]).map(([dir]) => dir);
78407
78752
  }
78408
- async function runMiningProjectsSetting() {
78753
+ async function runStudyingProjectsSetting() {
78409
78754
  const dirs = discoverProjectDirs();
78410
78755
  if (dirs.length === 0) {
78411
78756
  log2.info("No local agent sessions found yet; nothing to scope.");
@@ -78413,7 +78758,7 @@ async function runMiningProjectsSetting() {
78413
78758
  }
78414
78759
  const current = loadSyncState().project_filter;
78415
78760
  const selected = await multiselect2({
78416
- message: "Mine sessions from which folders?",
78761
+ message: "Study sessions from which folders?",
78417
78762
  options: dirs.map((dir) => ({
78418
78763
  label: dir === UNKNOWN_PROJECT ? "(unknown folder)" : displayDir(dir),
78419
78764
  value: dir
@@ -78428,7 +78773,7 @@ async function runMiningProjectsSetting() {
78428
78773
  const all = selected.length === dirs.length;
78429
78774
  saveSyncState(all ? state : { ...state, project_filter: [...selected] });
78430
78775
  const scope = all ? "all folders" : selected.map(displayDir).join(", ");
78431
- log2.success(`Mining scope ${dim(`· ${scope}`)}`);
78776
+ log2.success(`Study scope ${dim(`· ${scope}`)}`);
78432
78777
  }
78433
78778
  async function handleAuthenticate(cfg) {
78434
78779
  const shouldLogin = await confirm2({ message: "Open browser to log in?" });
@@ -78474,6 +78819,7 @@ function handleLogout(cfg) {
78474
78819
  }
78475
78820
  var import_picocolors35, ESC10, CLEAR_SCREEN;
78476
78821
  var init_tui = __esm(() => {
78822
+ init_client();
78477
78823
  init_config();
78478
78824
  init_agents();
78479
78825
  init_providers();
@@ -79131,7 +79477,7 @@ import {
79131
79477
  constants as constants3,
79132
79478
  fstatSync,
79133
79479
  openSync as openSync5,
79134
- readFileSync as readFileSync25,
79480
+ readFileSync as readFileSync26,
79135
79481
  readSync as readSync4,
79136
79482
  unlinkSync as unlinkSync5
79137
79483
  } from "node:fs";
@@ -82333,23 +82679,23 @@ ${results.length - opts.limit} more results not shown.`));
82333
82679
  console.log(` ID: ${store.id}`);
82334
82680
  console.log(` Space ID: ${store.space_id}`);
82335
82681
  });
82336
- cmd.command("sessions").description("List local agent sessions with full project and session ids (the untruncated view of the Activity screen's tabs)").option("--queued", "Only sessions queued for mining").option("--open", "Only live sessions still inside the quiet period").option("--mined", "Only recent mined-session history").option("--json", "Output as JSON").action((opts) => {
82337
- const all = !opts.queued && !opts.open && !opts.mined;
82682
+ cmd.command("sessions").description("List local agent sessions with full project and session ids (the untruncated view of the Activity screen's tabs)").option("--queued", "Only sessions queued for studying").option("--open", "Only live sessions still inside the quiet period").option("--studied", "Only recent studied-session history").option("--json", "Output as JSON").action((opts) => {
82683
+ const all = !opts.queued && !opts.open && !opts.studied;
82338
82684
  const wantQueued = all || Boolean(opts.queued);
82339
82685
  const wantOpen = all || Boolean(opts.open);
82340
- const wantMined = all || Boolean(opts.mined);
82686
+ const wantStudied = all || Boolean(opts.studied);
82341
82687
  const backlog = wantQueued || wantOpen ? listSessionBacklog() : { queued: [], open: [] };
82342
- const mined = wantMined ? loadSyncState().mined_sessions ?? [] : [];
82688
+ const studied = wantStudied ? loadSyncState().mined_sessions ?? [] : [];
82343
82689
  if (opts.json) {
82344
82690
  printResult({
82345
82691
  ...wantQueued ? { queued: backlog.queued } : {},
82346
82692
  ...wantOpen ? { open: backlog.open } : {},
82347
- ...wantMined ? { mined } : {}
82693
+ ...wantStudied ? { studied } : {}
82348
82694
  }, opts);
82349
82695
  return;
82350
82696
  }
82351
82697
  const sessionRows = (sessions) => sessions.map((s) => [s.harness, s.updated, s.project ?? "-", s.id]);
82352
- const minedRows = mined.map((record2) => {
82698
+ const studiedRows = studied.map((record2) => {
82353
82699
  const slash = record2.session.indexOf("/");
82354
82700
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
82355
82701
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
@@ -82373,11 +82719,11 @@ ${results.length - opts.limit} more results not shown.`));
82373
82719
  if (wantOpen) {
82374
82720
  section("Open", sessionRows(backlog.open), "Updated", "No open sessions. Live agent sessions sit here until they go quiet.");
82375
82721
  }
82376
- if (wantMined) {
82377
- section("Mined", minedRows, "Mined at", "No mined sessions recorded yet.");
82722
+ if (wantStudied) {
82723
+ section("Studied", studiedRows, "Studied at", "No studied sessions recorded yet.");
82378
82724
  }
82379
82725
  });
82380
- cmd.command("sync").description("Scan local agent session history and report the mining backlog").option("--quiet", "Background mode for hooks: honor backoff, exit 0, print nothing").option("--detach", "Re-spawn detached and return immediately (used by agent hooks)").option("--bootstrap", "Backfill mode: mine the full local session history regardless of age and drain the backlog (used by setup)").option("--status", "Show whether a sync is running now, plus watermark and recent activity").option("--report", "Write the same HTML harvest report as the log-to-dosu-knowledge skill and open it").option("--out <path>", "HTML report path (default: tmp/dosu-knowledge-report.html)").option("--json", "Output as JSON").action(async (opts) => {
82726
+ cmd.command("sync").description("Scan local agent session history and report the study backlog").option("--quiet", "Background mode for hooks: honor backoff, exit 0, print nothing").option("--detach", "Re-spawn detached and return immediately (used by agent hooks)").option("--bootstrap", "Backfill mode: study the full local session history regardless of age and drain the backlog (used by setup)").option("--status", "Show whether a sync is running now, plus watermark and recent activity").option("--report", "Write the same HTML harvest report as the log-to-dosu-knowledge skill and open it").option("--out <path>", "HTML report path (default: tmp/dosu-knowledge-report.html)").option("--json", "Output as JSON").action(async (opts) => {
82381
82727
  if (opts.status) {
82382
82728
  const status = getSyncStatus();
82383
82729
  if (opts.json) {
@@ -82398,7 +82744,7 @@ ${results.length - opts.limit} more results not shown.`));
82398
82744
  ]);
82399
82745
  return;
82400
82746
  }
82401
- const deps = { mine: buildMiner(opts.quiet ? "hook" : "manual") };
82747
+ const deps = { mine: buildLearner(opts.quiet ? "hook" : "manual") };
82402
82748
  let outcome = await runKnowledgeSync({
82403
82749
  quiet: opts.quiet,
82404
82750
  bootstrap: opts.bootstrap,
@@ -82406,7 +82752,7 @@ ${results.length - opts.limit} more results not shown.`));
82406
82752
  });
82407
82753
  if (opts.bootstrap && deps.mine) {
82408
82754
  const maxRounds = Math.ceil(outcome.readySessions / MINE_BATCH_LIMIT) + 2;
82409
- for (let round = 1;outcome.status === "mined" && round < maxRounds; round++) {
82755
+ for (let round = 1;outcome.status === "studied" && round < maxRounds; round++) {
82410
82756
  if (!opts.quiet && !opts.json)
82411
82757
  printSyncOutcome(outcome);
82412
82758
  outcome = await runKnowledgeSync({ quiet: opts.quiet, bootstrap: true, deps });
@@ -82463,12 +82809,12 @@ ${results.length - opts.limit} more results not shown.`));
82463
82809
  console.log("All your notes already have a transcript — nothing to backfill.");
82464
82810
  return;
82465
82811
  }
82466
- console.log(`Attributed ${result.updated} of ${result.candidates} notes (${result.ambiguous} ambiguous, ${result.noBatch} without a local mining batch). Run 'dosu knowledge report' to see their traces.`);
82812
+ console.log(`Attributed ${result.updated} of ${result.candidates} notes (${result.ambiguous} ambiguous, ${result.noBatch} without a local study batch). Run 'dosu knowledge report' to see their traces.`);
82467
82813
  });
82468
82814
  cmd.addCommand(hooksCommand());
82469
82815
  return cmd;
82470
82816
  }
82471
- function buildMiner(trigger) {
82817
+ function buildLearner(trigger) {
82472
82818
  const cfg = loadConfig();
82473
82819
  if (cfg.mode === "oss")
82474
82820
  return;
@@ -82477,8 +82823,8 @@ function buildMiner(trigger) {
82477
82823
  return;
82478
82824
  const { api_key, deployment_id } = target;
82479
82825
  return async (sessions) => {
82480
- const { runMiner: runMiner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82481
- return runMiner2({ sessions, apiKey: api_key, deploymentID: deployment_id, trigger });
82826
+ const { runLearner: runLearner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82827
+ return runLearner2({ sessions, apiKey: api_key, deploymentID: deployment_id, trigger });
82482
82828
  };
82483
82829
  }
82484
82830
  function formatAge(iso, now) {
@@ -82504,14 +82850,14 @@ function printSyncStatus(status, now = new Date) {
82504
82850
  console.log("○ No sync running.");
82505
82851
  }
82506
82852
  if (status.state.paused) {
82507
- console.log(import_picocolors18.default.yellow(" Mining paused: stopped by you. Resume from the Activity screen or run 'dosu knowledge sync'."));
82853
+ console.log(import_picocolors18.default.yellow(" Studying paused: stopped by you. Resume from the Activity screen or run 'dosu knowledge sync'."));
82508
82854
  }
82509
82855
  const wm2 = status.state.watermark;
82510
- console.log(` Mined through: ${wm2 ? `${wm2} (${formatAge(wm2, now)})` : "nothing mined yet"}`);
82856
+ console.log(` Studied through: ${wm2 ? `${wm2} (${formatAge(wm2, now)})` : "nothing studied yet"}`);
82511
82857
  if (status.state.project_filter?.length) {
82512
82858
  const home = homedir5();
82513
82859
  const scope = status.state.project_filter.map((dir) => dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir).join(", ");
82514
- console.log(` Mining scope: ${scope}`);
82860
+ console.log(` Study scope: ${scope}`);
82515
82861
  }
82516
82862
  if ((status.state.total_notes ?? 0) > 0) {
82517
82863
  const tokens = status.state.total_learning_tokens ?? 0;
@@ -82526,7 +82872,7 @@ function printSyncStatus(status, now = new Date) {
82526
82872
  console.log(import_picocolors18.default.yellow(` Backing off after ${n5} failure${n5 === 1 ? "" : "s"}; background syncs retry after ${status.backoffUntil}.`));
82527
82873
  }
82528
82874
  if (status.state.last_refusal) {
82529
- console.log(import_picocolors18.default.yellow(` Mining paused: ${status.state.last_refusal.message} (${formatAge(status.state.last_refusal.at, now)})`));
82875
+ console.log(import_picocolors18.default.yellow(` Studying paused: ${status.state.last_refusal.message} (${formatAge(status.state.last_refusal.at, now)})`));
82530
82876
  }
82531
82877
  if (status.recentActivity.length > 0) {
82532
82878
  console.log(`
@@ -82542,25 +82888,25 @@ function printSyncOutcome(outcome) {
82542
82888
  switch (outcome.status) {
82543
82889
  case "backlog": {
82544
82890
  const inFlight = outcome.inFlightSessions > 0 ? import_picocolors18.default.dim(` (${outcome.inFlightSessions} more still in progress)`) : "";
82545
- console.log(`✓ Scanned. ${outcome.readySessions} new session${outcome.readySessions === 1 ? "" : "s"} ready to mine${inFlight}.`);
82546
- console.log(import_picocolors18.default.dim("Sign in with 'dosu setup' to enable mining."));
82891
+ console.log(`✓ Scanned. ${outcome.readySessions} new session${outcome.readySessions === 1 ? "" : "s"} ready to study${inFlight}.`);
82892
+ console.log(import_picocolors18.default.dim("Sign in with 'dosu setup' to enable studying."));
82547
82893
  break;
82548
82894
  }
82549
- case "mined": {
82550
- const notes = outcome.miner?.notesWritten ?? 0;
82551
- const remaining = outcome.readySessions - (outcome.minedSessions ?? 0);
82552
- console.log(`✓ Mined ${outcome.minedSessions} session${outcome.minedSessions === 1 ? "" : "s"}, ${notes} suggested page${notes === 1 ? "" : "s"} created.`);
82895
+ case "studied": {
82896
+ const notes = outcome.learner?.notesWritten ?? 0;
82897
+ const remaining = outcome.readySessions - (outcome.studiedSessions ?? 0);
82898
+ console.log(`✓ Studied ${outcome.studiedSessions} session${outcome.studiedSessions === 1 ? "" : "s"}, ${notes} suggested page${notes === 1 ? "" : "s"} created.`);
82553
82899
  if (remaining > 0) {
82554
82900
  console.log(import_picocolors18.default.dim(`${remaining} more in the backlog; run sync again to continue.`));
82555
82901
  }
82556
82902
  break;
82557
82903
  }
82558
82904
  case "skipped-gateway": {
82559
- console.log(import_picocolors18.default.yellow(outcome.miner?.message ?? "Mining unavailable right now."));
82905
+ console.log(import_picocolors18.default.yellow(outcome.learner?.message ?? "Studying unavailable right now."));
82560
82906
  break;
82561
82907
  }
82562
82908
  case "mine-failed": {
82563
- console.error(import_picocolors18.default.red(outcome.miner?.message ?? "Mining run failed."));
82909
+ console.error(import_picocolors18.default.red(outcome.learner?.message ?? "Study run failed."));
82564
82910
  process.exitCode = 1;
82565
82911
  break;
82566
82912
  }
@@ -82583,7 +82929,7 @@ function printSyncOutcome(outcome) {
82583
82929
  break;
82584
82930
  }
82585
82931
  case "skipped-paused": {
82586
- console.log(import_picocolors18.default.dim("Skipped: mining is paused. Run 'dosu knowledge sync' to resume."));
82932
+ console.log(import_picocolors18.default.dim("Skipped: studying is paused. Run 'dosu knowledge sync' to resume."));
82587
82933
  break;
82588
82934
  }
82589
82935
  }
@@ -83335,7 +83681,10 @@ init_skill();
83335
83681
  // src/commands/sources.ts
83336
83682
  init_commander();
83337
83683
  init_trpc();
83684
+ init_github_step();
83685
+ init_installation_server();
83338
83686
  var import_picocolors25 = __toESM(require_picocolors(), 1);
83687
+ var CONNECT_TIMEOUT_DEFAULT_SECONDS = 600;
83339
83688
  function requireConfig11() {
83340
83689
  const cfg = requireLoginConfig();
83341
83690
  if (!cfg.active_account?.target?.org_id) {
@@ -83344,6 +83693,15 @@ function requireConfig11() {
83344
83693
  }
83345
83694
  return cfg;
83346
83695
  }
83696
+ function rejectWebOnlyProvider(provider, json2) {
83697
+ const url2 = getWebAppURL();
83698
+ if (json2) {
83699
+ printResult({ provider, cli_supported: false, connect_via: "web", url: url2 }, { json: true });
83700
+ } else {
83701
+ console.error(import_picocolors25.default.yellow(`Connecting '${provider}' sources is web-only for now. Open ${url2}, add the source ` + `under Data Sources, then re-run 'dosu sources list'.`));
83702
+ }
83703
+ process.exit(1);
83704
+ }
83347
83705
  function sourcesCommand() {
83348
83706
  const cmd = new Command("sources").description("Manage connected data sources");
83349
83707
  cmd.command("list").description("List all connected data sources").option("--json", "Output as JSON").action(async (opts) => {
@@ -83426,6 +83784,154 @@ function sourcesCommand() {
83426
83784
  }
83427
83785
  console.log(import_picocolors25.default.green("Data source deleted."));
83428
83786
  });
83787
+ cmd.command("connect").description("Install or update a provider integration (currently GitHub) via the browser").argument("[provider]", "Provider to connect (github)", "github").option("--json", "Emit machine-readable NDJSON progress events").option("--timeout <seconds>", "Seconds to wait for the browser install to complete", String(CONNECT_TIMEOUT_DEFAULT_SECONDS)).option("--no-open", "Print the URL without opening a browser").action(async (provider, opts) => {
83788
+ const providerName = provider.toLowerCase();
83789
+ if (providerName !== "github") {
83790
+ rejectWebOnlyProvider(providerName, opts.json);
83791
+ return;
83792
+ }
83793
+ const cfg = requireConfig11();
83794
+ const client = createTypedClient(cfg);
83795
+ const orgID = cfg.active_account.target.org_id;
83796
+ const timeoutSeconds = Math.max(1, Number.parseInt(opts.timeout ?? "", 10)) || CONNECT_TIMEOUT_DEFAULT_SECONDS;
83797
+ const before = await fetchListForOrg(client, orgID);
83798
+ const { server, installationPromise } = await startInstallationCallbackServer();
83799
+ let timeoutId;
83800
+ try {
83801
+ const connectURL = new URL("/cli/connect-github", getWebAppURL());
83802
+ connectURL.searchParams.set("callback", `http://localhost:${server.port}/callback`);
83803
+ const url2 = connectURL.toString();
83804
+ if (opts.json) {
83805
+ console.log(JSON.stringify({
83806
+ event: "awaiting_install",
83807
+ provider: "github",
83808
+ url: url2,
83809
+ timeout_seconds: timeoutSeconds
83810
+ }));
83811
+ } else {
83812
+ console.log(`Open this URL to install or update the Dosu GitHub App:
83813
+ ${import_picocolors25.default.cyan(url2)}`);
83814
+ if (opts.open !== false) {
83815
+ try {
83816
+ const open5 = await Promise.resolve().then(() => (init_open(), exports_open));
83817
+ await open5.default(url2);
83818
+ } catch {}
83819
+ }
83820
+ console.log(import_picocolors25.default.dim("Waiting for the install to complete..."));
83821
+ }
83822
+ const timedOut = new Promise((resolve6) => {
83823
+ timeoutId = setTimeout(() => resolve6(null), timeoutSeconds * 1000);
83824
+ });
83825
+ const result = await Promise.race([installationPromise, timedOut]);
83826
+ if (result === null) {
83827
+ if (opts.json) {
83828
+ console.log(JSON.stringify({ event: "timeout", timeout_seconds: timeoutSeconds }));
83829
+ } else {
83830
+ console.error(import_picocolors25.default.red(`Timed out after ${timeoutSeconds}s. Finish the install in the browser, then re-run 'dosu sources connect github'.`));
83831
+ }
83832
+ process.exit(1);
83833
+ return;
83834
+ }
83835
+ const refresh = await waitForRepositoryRefresh(client, orgID, before);
83836
+ const beforeIds = new Set(before.map((repo) => repo.repository_id));
83837
+ const newRepositories = refresh.repos.filter((repo) => !beforeIds.has(repo.repository_id)).map((repo) => ({ slug: repo.slug, repository_id: repo.repository_id }));
83838
+ if (opts.json) {
83839
+ console.log(JSON.stringify({
83840
+ event: "installed",
83841
+ installation_id: result.installation_id,
83842
+ new_repositories: newRepositories
83843
+ }));
83844
+ return;
83845
+ }
83846
+ console.log(import_picocolors25.default.green(`GitHub App connected (installation ${result.installation_id}).`));
83847
+ if (newRepositories.length > 0) {
83848
+ for (const repo of newRepositories) {
83849
+ console.log(` ${repo.slug}`);
83850
+ }
83851
+ console.log(import_picocolors25.default.dim("Next: dosu sources create github --repo <owner/name> --library <library-id>"));
83852
+ } else {
83853
+ console.log(import_picocolors25.default.dim("No new repositories visible yet — GitHub may still be syncing. " + "Re-run 'dosu sources connect github' or check 'dosu sources list' shortly."));
83854
+ }
83855
+ } finally {
83856
+ clearTimeout(timeoutId);
83857
+ server.close();
83858
+ }
83859
+ });
83860
+ cmd.command("create").description("Create a data source from an installed provider and attach it to a library (requires confirmation)").argument("<provider>", "Provider (currently: github)").requiredOption("--repo <owner/name>", "GitHub repository slug visible to the Dosu app").option("--library <id>", "Library (space) ID to attach to; defaults to the active library").option("--confirm", "Apply without the interactive prompt").option("--json", "Output as JSON").action(async (provider, opts) => {
83861
+ const providerName = provider.toLowerCase();
83862
+ if (providerName !== "github") {
83863
+ rejectWebOnlyProvider(providerName, opts.json);
83864
+ return;
83865
+ }
83866
+ const cfg = requireConfig11();
83867
+ const orgID = cfg.active_account.target.org_id;
83868
+ const libraryID = opts.library ?? cfg.active_account?.target?.space_id;
83869
+ if (!libraryID) {
83870
+ console.error(import_picocolors25.default.red("No library specified and no active library. Pass --library <id>."));
83871
+ process.exit(1);
83872
+ return;
83873
+ }
83874
+ const client = createTypedClient(cfg);
83875
+ const repos = await fetchListForOrg(client, orgID);
83876
+ const repo = repos.find((r9) => r9.slug.toLowerCase() === opts.repo.toLowerCase());
83877
+ if (!repo) {
83878
+ console.error(import_picocolors25.default.red(`Repository '${opts.repo}' is not visible to Dosu. Run ` + `'dosu sources connect github' to grant the GitHub App access, then retry.`));
83879
+ process.exit(1);
83880
+ return;
83881
+ }
83882
+ if (repo.is_fork === true) {
83883
+ console.error(import_picocolors25.default.red(`'${repo.slug}' is a fork and can't be synced.` + (repo.fork_parent_slug ? ` Connect '${repo.fork_parent_slug}' instead.` : "")));
83884
+ process.exit(1);
83885
+ return;
83886
+ }
83887
+ if (!await confirmAction({
83888
+ confirmed: opts.confirm,
83889
+ json: opts.json,
83890
+ message: `Create a data source for ${repo.slug} and attach it to library ${libraryID}?`,
83891
+ preview: {
83892
+ action: "create_source",
83893
+ provider: "github",
83894
+ repository: repo.slug,
83895
+ repository_id: repo.repository_id,
83896
+ library_id: libraryID
83897
+ }
83898
+ }))
83899
+ return;
83900
+ const existingDeployments = await fetchOrgGithubDeployments(client, orgID);
83901
+ const existingDataSources = await fetchOrgGithubDataSources(client, orgID);
83902
+ const created = await createDeploymentForRepo(client, orgID, libraryID, repo, {
83903
+ deploymentID: existingDeployments.get(repo.repository_id),
83904
+ dataSourceID: existingDataSources.get(repo.repository_id)
83905
+ });
83906
+ if (!created) {
83907
+ console.error(import_picocolors25.default.red("Could not create the data source. Check 'dosu logs --tail 50' for details."));
83908
+ process.exit(1);
83909
+ return;
83910
+ }
83911
+ const survivors = await verifyDataSourcesPersist(client, orgID, [created.data_source_id]);
83912
+ if (survivors.dropped.size > 0) {
83913
+ await deleteOrphanDeployment(client, created.deployment_id, repo.slug);
83914
+ console.error(import_picocolors25.default.red(`Dosu couldn't sync '${repo.slug}' (the GitHub App may not have access to it). ` + "Nothing was created."));
83915
+ process.exit(1);
83916
+ return;
83917
+ }
83918
+ const receipt = {
83919
+ provider: "github",
83920
+ repository: repo.slug,
83921
+ repository_id: repo.repository_id,
83922
+ data_source_id: created.data_source_id,
83923
+ deployment_id: created.deployment_id,
83924
+ library_id: libraryID,
83925
+ attached: true
83926
+ };
83927
+ if (opts.json) {
83928
+ printResult(receipt, opts);
83929
+ return;
83930
+ }
83931
+ console.log(import_picocolors25.default.green(`Connected ${repo.slug} to library ${libraryID}.`));
83932
+ console.log(import_picocolors25.default.dim(` data source ${created.data_source_id}`));
83933
+ console.log(import_picocolors25.default.dim(` deployment ${created.deployment_id}`));
83934
+ });
83429
83935
  return cmd;
83430
83936
  }
83431
83937
 
@@ -83939,10 +84445,12 @@ function commandTelemetryContext() {
83939
84445
  const tokenUserID = getAccessTokenUserID(accessToken);
83940
84446
  const userID = configUserID && configUserID === tokenUserID ? configUserID : undefined;
83941
84447
  const email3 = userID ? getAccessTokenEmail(accessToken) : undefined;
84448
+ const orgId = userID ? cfg.active_account?.target?.org_id : undefined;
83942
84449
  return {
83943
84450
  mode: cfg.mode === MODE_OSS ? "oss" : "cloud",
83944
84451
  isAuthenticated: authenticated,
83945
- ...userID ? { user: { id: userID, ...email3 ? { email: email3 } : {} } } : {}
84452
+ ...userID ? { user: { id: userID, ...email3 ? { email: email3 } : {} } } : {},
84453
+ ...orgId ? { orgId } : {}
83946
84454
  };
83947
84455
  } catch {
83948
84456
  return { mode: "cloud", isAuthenticated: false };
@@ -84341,7 +84849,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84341
84849
  }
84342
84850
  if (opts.follow) {
84343
84851
  try {
84344
- const lines = readFileSync25(logPath, "utf-8").split(`
84852
+ const lines = readFileSync26(logPath, "utf-8").split(`
84345
84853
  `);
84346
84854
  if (lines.at(-1) === "")
84347
84855
  lines.pop();
@@ -84357,7 +84865,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84357
84865
  if (opts.tail !== undefined) {
84358
84866
  const n5 = typeof opts.tail === "string" ? parseInt(opts.tail, 10) || 50 : 50;
84359
84867
  try {
84360
- const content = readFileSync25(logPath, "utf-8");
84868
+ const content = readFileSync26(logPath, "utf-8");
84361
84869
  const lines = content.split(`
84362
84870
  `);
84363
84871
  console.log(lines.slice(-n5).join(`
@@ -84428,4 +84936,4 @@ execute().catch((err) => {
84428
84936
  process.exit(1);
84429
84937
  });
84430
84938
 
84431
- //# debugId=971484cf-da0f-e0c4-6475-6e2164756e21
84939
+ //# debugId=e98d2bf5-99f8-402d-6475-6e2164756e21