@dosu/cli 0.53.0-beta.8 → 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 +1166 -643
  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.8", 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,8 +8072,7 @@ 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;
8064
8076
  continue;
8065
8077
  }
8066
8078
  result.push(data[i2]);
@@ -8068,6 +8080,43 @@ function stripJSONComments(data) {
8068
8080
  }
8069
8081
  return result.join("");
8070
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);
8104
+ continue;
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
+ }
8115
+ result.push(data[i2]);
8116
+ i2++;
8117
+ }
8118
+ return result.join("");
8119
+ }
8071
8120
  function saveJSONConfig(path3, cfg) {
8072
8121
  writeSecureFile(path3, JSON.stringify(cfg, null, 2));
8073
8122
  }
@@ -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,18 +11020,18 @@ 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:
10978
11027
 
10979
11028
  ${list}
10980
11029
 
10981
- Read each session with read_session, decide what (if anything) is durable per your rules, dedupe against read_knowledge, then write the distilled notes with write_knowledge. When you are done, reply with a one-line summary: how many sessions you read and how many notes you wrote.`;
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();
@@ -72926,9 +72974,10 @@ async function runMiner(options) {
72926
72974
  const abort = new AbortController;
72927
72975
  const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
72928
72976
  let notesWritten = 0;
72929
- let lastSessionId;
72977
+ let currentSession;
72978
+ const readsSinceWrite = new Set;
72930
72979
  let turns = 0;
72931
- const env = buildMinerEnv({
72980
+ const env = buildLearnerEnv({
72932
72981
  apiKey: options.apiKey,
72933
72982
  gatewayURL,
72934
72983
  configDir: configDir.path,
@@ -72939,9 +72988,9 @@ async function runMiner(options) {
72939
72988
  });
72940
72989
  try {
72941
72990
  const run2 = query({
72942
- prompt: buildMinerPrompt(options.sessions),
72991
+ prompt: buildLearnerPrompt(options.sessions),
72943
72992
  options: {
72944
- systemPrompt: buildMinerSystemPrompt(MINER_CORE_RULES),
72993
+ systemPrompt: buildLearnerSystemPrompt(LEARNER_CORE_RULES),
72945
72994
  env,
72946
72995
  abortController: abort,
72947
72996
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
@@ -72957,21 +73006,25 @@ async function runMiner(options) {
72957
73006
  headers: {
72958
73007
  ...mcpHeaders(options.apiKey),
72959
73008
  "X-Dosu-Session-Id": runID,
72960
- "X-Dosu-Client": `dosu-cli-miner/${getVersionString()}`,
73009
+ "X-Dosu-Client": `dosu-cli-learner/${getVersionString()}`,
72961
73010
  ...sessionStartedAt ? { "X-Dosu-Session-Started-At": sessionStartedAt } : {}
72962
73011
  }
72963
73012
  }
72964
73013
  },
72965
- stderr: (data) => logger.debug("miner", `[sdk] ${data}`),
73014
+ stderr: (data) => logger.debug("learner", `[sdk] ${data}`),
72966
73015
  canUseTool: async (toolName, input2) => {
72967
73016
  if (!allowed.has(toolName)) {
72968
73017
  return {
72969
73018
  behavior: "deny",
72970
- message: `Tool ${toolName} is not permitted in mining runs.`
73019
+ message: `Tool ${toolName} is not permitted in study runs.`
72971
73020
  };
72972
73021
  }
72973
73022
  if (toolName === `mcp__${SESSIONS_SERVER_NAME}__read_session`) {
72974
- lastSessionId = sessionIdFromReadInput(input2) ?? lastSessionId;
73023
+ const id2 = sessionIdFromReadInput(input2);
73024
+ if (id2) {
73025
+ currentSession = id2;
73026
+ readsSinceWrite.add(id2);
73027
+ }
72975
73028
  }
72976
73029
  if (toolName === `mcp__${KNOWLEDGE_SERVER_NAME}__write_knowledge`) {
72977
73030
  if (notesWritten >= maxNotes) {
@@ -72980,10 +73033,20 @@ async function runMiner(options) {
72980
73033
  message: `Note cap reached (${maxNotes} per run); stop writing and summarize.`
72981
73034
  };
72982
73035
  }
73036
+ if (readsSinceWrite.size > 1) {
73037
+ readsSinceWrite.clear();
73038
+ currentSession = undefined;
73039
+ return {
73040
+ behavior: "deny",
73041
+ message: "Write each session's notes right after reading THAT session, before reading the next. You have read multiple sessions without writing, so a note cannot be attributed to one. Re-read only the session this note is about, then write it."
73042
+ };
73043
+ }
72983
73044
  notesWritten += 1;
73045
+ readsSinceWrite.clear();
73046
+ const { transcript_id: _authoredByModel, ...clean } = input2;
72984
73047
  return {
72985
73048
  behavior: "allow",
72986
- updatedInput: lastSessionId ? { ...input2, transcript_id: lastSessionId } : input2
73049
+ updatedInput: currentSession ? { ...clean, transcript_id: currentSession } : clean
72987
73050
  };
72988
73051
  }
72989
73052
  return { behavior: "allow", updatedInput: input2 };
@@ -73005,15 +73068,15 @@ async function runMiner(options) {
73005
73068
  };
73006
73069
  }
73007
73070
  if (message.subtype !== "success" || message.is_error) {
73008
- logger.debug("miner", `run ${runID} failed: ${text}`);
73071
+ logger.debug("learner", `run ${runID} failed: ${text}`);
73009
73072
  return {
73010
73073
  outcome: "error",
73011
73074
  notesWritten,
73012
73075
  turns,
73013
- message: "Mining run failed; see debug log for details."
73076
+ message: "Study run failed; see debug log for details."
73014
73077
  };
73015
73078
  }
73016
- logger.debug("miner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73079
+ logger.debug("learner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73017
73080
  return { outcome: "completed", notesWritten, turns, message: text };
73018
73081
  }
73019
73082
  }
@@ -73021,7 +73084,7 @@ async function runMiner(options) {
73021
73084
  outcome: "error",
73022
73085
  notesWritten,
73023
73086
  turns,
73024
- message: "Mining run ended without a result."
73087
+ message: "Study run ended without a result."
73025
73088
  };
73026
73089
  } catch (error61) {
73027
73090
  const text = error61 instanceof Error ? error61.message : String(error61);
@@ -73029,12 +73092,12 @@ async function runMiner(options) {
73029
73092
  if (gatewayError) {
73030
73093
  return { outcome: gatewayError.outcome, notesWritten, turns, message: gatewayError.message };
73031
73094
  }
73032
- logger.debug("miner", `run ${runID} threw: ${text}`);
73095
+ logger.debug("learner", `run ${runID} threw: ${text}`);
73033
73096
  return {
73034
73097
  outcome: "error",
73035
73098
  notesWritten,
73036
73099
  turns,
73037
- 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."
73038
73101
  };
73039
73102
  } finally {
73040
73103
  clearTimeout(timer);
@@ -73504,7 +73567,7 @@ var init_settings = __esm(() => {
73504
73567
  });
73505
73568
 
73506
73569
  // src/telemetry/debug-id.ts
73507
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "cf222ab5-3069-6407-6475-6e2164756e21";
73570
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "e98d2bf5-99f8-402d-6475-6e2164756e21";
73508
73571
 
73509
73572
  // src/telemetry/telemetry.ts
73510
73573
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -73533,11 +73596,13 @@ function normalizeContext(context) {
73533
73596
  const isAuthenticated2 = context?.isAuthenticated === true;
73534
73597
  const id2 = safeRead(context?.user, "id");
73535
73598
  const email3 = safeRead(context?.user, "email");
73536
- 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;
73537
73601
  return {
73538
73602
  mode: context?.mode === "oss" ? "oss" : "cloud",
73539
73603
  isAuthenticated: isAuthenticated2,
73540
- ...user ? { user } : {}
73604
+ ...user ? { user } : {},
73605
+ ...user && isUUID2(orgId) ? { orgId } : {}
73541
73606
  };
73542
73607
  }
73543
73608
  function normalizeExitCode(value, fallback = 1) {
@@ -73557,12 +73622,9 @@ function normalizeRuntime(runtime) {
73557
73622
  isTty: runtime.isTty === true
73558
73623
  };
73559
73624
  }
73560
- function validInstallId(value) {
73625
+ function isUUID2(value) {
73561
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);
73562
73627
  }
73563
- function validUserId(value) {
73564
- return validInstallId(value);
73565
- }
73566
73628
  function validEmail(value) {
73567
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);
73568
73630
  }
@@ -73712,7 +73774,7 @@ function buildPostHogPayload(input2) {
73712
73774
  const result = input2.result === "success" || input2.result === "validation_error" ? input2.result : "failure";
73713
73775
  return {
73714
73776
  api_key: input2.apiKey,
73715
- 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),
73716
73778
  event: "cli_command_completed",
73717
73779
  properties: {
73718
73780
  $geoip_disable: true,
@@ -73730,6 +73792,10 @@ function buildPostHogPayload(input2) {
73730
73792
  is_ci: runtime.isCi,
73731
73793
  is_tty: runtime.isTty,
73732
73794
  mode: context.mode,
73795
+ ...context.orgId ? {
73796
+ org_id: context.orgId,
73797
+ $groups: { organization: context.orgId }
73798
+ } : {},
73733
73799
  is_authenticated: context.isAuthenticated,
73734
73800
  exit_code: normalizeExitCode(input2.exitCode),
73735
73801
  ...errorCode ? { error_code: errorCode } : {}
@@ -73844,7 +73910,12 @@ function buildSentryEnvelope(input2) {
73844
73910
  level: "error",
73845
73911
  release: `dosu-cli@${runtime.version}`,
73846
73912
  tags: sentryTags(command, context, runtime, error61),
73847
- ...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
+ } : {},
73848
73919
  ...mappedBundle ? {
73849
73920
  debug_meta: {
73850
73921
  images: [{ type: "sourcemap", code_file: BUNDLE_CODE_FILE, debug_id: debugId }]
@@ -74038,7 +74109,7 @@ function safeNow(now) {
74038
74109
  function safeUuid(generate) {
74039
74110
  try {
74040
74111
  const value = generate();
74041
- return validInstallId(value) ? value : undefined;
74112
+ return isUUID2(value) ? value : undefined;
74042
74113
  } catch {
74043
74114
  return;
74044
74115
  }
@@ -74070,7 +74141,7 @@ function createCommandTelemetry(settings2, dependencies = {}) {
74070
74141
  let installIdResolved = false;
74071
74142
  let resolvedInstallId = settings2.install_id;
74072
74143
  function installId() {
74073
- if (validInstallId(resolvedInstallId))
74144
+ if (isUUID2(resolvedInstallId))
74074
74145
  return resolvedInstallId;
74075
74146
  if (installIdResolved)
74076
74147
  return;
@@ -74080,7 +74151,7 @@ function createCommandTelemetry(settings2, dependencies = {}) {
74080
74151
  } catch {
74081
74152
  resolvedInstallId = undefined;
74082
74153
  }
74083
- return validInstallId(resolvedInstallId) ? resolvedInstallId : undefined;
74154
+ return isUUID2(resolvedInstallId) ? resolvedInstallId : undefined;
74084
74155
  }
74085
74156
  function debugPayload(payload) {
74086
74157
  try {
@@ -74299,16 +74370,6 @@ var init_follow = () => {};
74299
74370
 
74300
74371
  // src/mcp/providers/base.ts
74301
74372
  function createJSONProvider(opts) {
74302
- const defaultBuildServer = (cfg) => ({
74303
- type: "http",
74304
- url: mcpURL(cfg.active_account.target.deployment_id),
74305
- headers: mcpHeaders(cfg.active_account?.target?.api_key)
74306
- });
74307
- const defaultBuildOSSServer = (cfg) => ({
74308
- type: "http",
74309
- url: mcpBaseURL(),
74310
- headers: mcpHeaders(cfg.active_account?.target?.api_key)
74311
- });
74312
74373
  const buildServer = opts.buildServer ?? defaultBuildServer;
74313
74374
  return {
74314
74375
  name: () => opts.providerName,
@@ -74320,8 +74381,10 @@ function createJSONProvider(opts) {
74320
74381
  globalConfigPath: () => expandHome(opts.globalPath),
74321
74382
  isConfigured: () => isJSONKeyConfigured(expandHome(opts.globalPath), opts.topKey),
74322
74383
  install(cfg, global2) {
74323
- if (cfg.mode !== MODE_OSS && !cfg.active_account?.target?.deployment_id)
74324
- throw new Error("deployment ID is required");
74384
+ const endpoint = {
74385
+ url: mcpEndpoint(cfg),
74386
+ headers: mcpHeaders(cfg.active_account?.target?.api_key)
74387
+ };
74325
74388
  let configPath;
74326
74389
  if (global2) {
74327
74390
  configPath = expandHome(opts.globalPath);
@@ -74330,8 +74393,7 @@ function createJSONProvider(opts) {
74330
74393
  } else {
74331
74394
  throw new Error(`${opts.providerName} does not support local installation`);
74332
74395
  }
74333
- const serverBuilder = cfg.mode === MODE_OSS ? defaultBuildOSSServer : buildServer;
74334
- installJSONServer(configPath, opts.topKey, serverBuilder(cfg));
74396
+ installJSONServer(configPath, opts.topKey, buildServer(endpoint, cfg));
74335
74397
  },
74336
74398
  remove(global2) {
74337
74399
  let configPath;
@@ -74346,8 +74408,12 @@ function createJSONProvider(opts) {
74346
74408
  }
74347
74409
  };
74348
74410
  }
74411
+ var defaultBuildServer = ({ url: url2, headers }) => ({
74412
+ type: "http",
74413
+ url: url2,
74414
+ headers
74415
+ });
74349
74416
  var init_base = __esm(() => {
74350
- init_config();
74351
74417
  init_config_helpers();
74352
74418
  init_detect();
74353
74419
  });
@@ -74361,13 +74427,12 @@ var AntigravityProvider = () => createJSONProvider({
74361
74427
  paths: ["~/.gemini"],
74362
74428
  globalPath: "~/.gemini/antigravity/mcp_config.json",
74363
74429
  topKey: "mcpServers",
74364
- buildServer: (cfg) => ({
74365
- serverUrl: mcpURL(cfg.active_account.target.deployment_id),
74366
- headers: mcpHeaders(cfg.active_account.target.api_key)
74430
+ buildServer: ({ url: url2, headers }) => ({
74431
+ serverUrl: url2,
74432
+ headers
74367
74433
  })
74368
74434
  });
74369
74435
  var init_antigravity = __esm(() => {
74370
- init_config_helpers();
74371
74436
  init_base();
74372
74437
  });
74373
74438
 
@@ -74404,9 +74469,7 @@ var ClaudeDesktopProvider = () => ({
74404
74469
  install(cfg, global2) {
74405
74470
  if (!global2)
74406
74471
  throw new Error("Claude Desktop does not support local installation");
74407
- if (cfg.mode !== MODE_OSS && !cfg.active_account?.target?.deployment_id)
74408
- throw new Error("deployment ID is required");
74409
- const url2 = cfg.mode === MODE_OSS ? mcpBaseURL() : mcpURL(cfg.active_account.target.deployment_id);
74472
+ const url2 = mcpEndpoint(cfg);
74410
74473
  const npx = findNpx();
74411
74474
  const remote = mcpRemoteServer(url2, cfg.active_account?.target?.api_key);
74412
74475
  installJSONServer(configPath(), "mcpServers", {
@@ -74422,7 +74485,6 @@ var ClaudeDesktopProvider = () => ({
74422
74485
  }
74423
74486
  });
74424
74487
  var init_claude_desktop = __esm(() => {
74425
- init_config();
74426
74488
  init_config_helpers();
74427
74489
  init_detect();
74428
74490
  });
@@ -74437,15 +74499,14 @@ var extensionDir = () => join25(appSupportDir(), "Code", "User", "globalStorage"
74437
74499
  paths: [extensionDir()],
74438
74500
  globalPath: join25(extensionDir(), "settings", "cline_mcp_settings.json"),
74439
74501
  topKey: "mcpServers",
74440
- buildServer: (cfg) => ({
74441
- url: mcpURL(cfg.active_account.target.deployment_id),
74502
+ buildServer: ({ url: url2, headers }) => ({
74503
+ url: url2,
74442
74504
  type: "streamableHttp",
74443
74505
  disabled: false,
74444
- headers: mcpHeaders(cfg.active_account.target.api_key)
74506
+ headers
74445
74507
  })
74446
74508
  });
74447
74509
  var init_cline = __esm(() => {
74448
- init_config_helpers();
74449
74510
  init_detect();
74450
74511
  init_base();
74451
74512
  });
@@ -74463,15 +74524,14 @@ var ClineCliProvider = () => createJSONProvider({
74463
74524
  paths: [clineDir()],
74464
74525
  globalPath: join26(clineDir(), "data", "settings", "cline_mcp_settings.json"),
74465
74526
  topKey: "mcpServers",
74466
- buildServer: (cfg) => ({
74467
- url: mcpURL(cfg.active_account.target.deployment_id),
74527
+ buildServer: ({ url: url2, headers }) => ({
74528
+ url: url2,
74468
74529
  type: "streamableHttp",
74469
74530
  disabled: false,
74470
- headers: mcpHeaders(cfg.active_account.target.api_key)
74531
+ headers
74471
74532
  })
74472
74533
  });
74473
74534
  var init_cline_cli = __esm(() => {
74474
- init_config_helpers();
74475
74535
  init_detect();
74476
74536
  init_base();
74477
74537
  });
@@ -74495,13 +74555,6 @@ function readTOML(path3) {
74495
74555
  function writeTOML(path3, content) {
74496
74556
  writeSecureFile(path3, content);
74497
74557
  }
74498
- function mcpEndpoint(cfg) {
74499
- if (cfg.mode === MODE_OSS)
74500
- return mcpBaseURL();
74501
- if (!cfg.active_account?.target?.deployment_id)
74502
- throw new Error("deployment ID is required");
74503
- return mcpURL(cfg.active_account?.target?.deployment_id);
74504
- }
74505
74558
  function tomlString(value) {
74506
74559
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
74507
74560
  }
@@ -74584,13 +74637,6 @@ function globalPath() {
74584
74637
  }
74585
74638
  return expandHome("~/.copilot/mcp-config.json");
74586
74639
  }
74587
- function mcpEndpoint2(cfg) {
74588
- if (cfg.mode === MODE_OSS)
74589
- return mcpBaseURL();
74590
- if (!cfg.active_account?.target?.deployment_id)
74591
- throw new Error("deployment ID is required");
74592
- return mcpURL(cfg.active_account?.target?.deployment_id);
74593
- }
74594
74640
  var CopilotProvider = () => ({
74595
74641
  name: () => "GitHub Copilot CLI",
74596
74642
  id: () => "copilot",
@@ -74601,7 +74647,7 @@ var CopilotProvider = () => ({
74601
74647
  globalConfigPath: () => globalPath(),
74602
74648
  isConfigured: () => isJSONKeyConfigured(globalPath(), "mcpServers"),
74603
74649
  install(cfg, global2) {
74604
- const url2 = mcpEndpoint2(cfg);
74650
+ const url2 = mcpEndpoint(cfg);
74605
74651
  if (global2) {
74606
74652
  const server = {
74607
74653
  type: "http",
@@ -74630,7 +74676,6 @@ var CopilotProvider = () => ({
74630
74676
  }
74631
74677
  });
74632
74678
  var init_copilot = __esm(() => {
74633
- init_config();
74634
74679
  init_config_helpers();
74635
74680
  init_detect();
74636
74681
  });
@@ -74645,14 +74690,13 @@ var CursorProvider = () => createJSONProvider({
74645
74690
  paths: ["~/.cursor"],
74646
74691
  globalPath: "~/.cursor/mcp.json",
74647
74692
  topKey: "mcpServers",
74648
- buildServer: (cfg) => ({
74649
- url: mcpURL(cfg.active_account.target.deployment_id),
74650
- headers: mcpHeaders(cfg.active_account.target.api_key)
74693
+ buildServer: ({ url: url2, headers }) => ({
74694
+ url: url2,
74695
+ headers
74651
74696
  }),
74652
74697
  localConfigPath: (cwd) => join29(cwd, ".cursor", "mcp.json")
74653
74698
  });
74654
74699
  var init_cursor = __esm(() => {
74655
- init_config_helpers();
74656
74700
  init_base();
74657
74701
  });
74658
74702
 
@@ -74689,13 +74733,6 @@ var init_gemini = __esm(() => {
74689
74733
  });
74690
74734
 
74691
74735
  // src/mcp/providers/manual.ts
74692
- function mcpEndpoint3(cfg) {
74693
- if (cfg.mode === MODE_OSS)
74694
- return mcpBaseURL();
74695
- if (!cfg.active_account?.target?.deployment_id)
74696
- throw new Error("deployment ID is required");
74697
- return mcpURL(cfg.active_account?.target?.deployment_id);
74698
- }
74699
74736
  function maskSecret(secret) {
74700
74737
  if (secret.length <= 8)
74701
74738
  return "[hidden]";
@@ -74707,7 +74744,7 @@ var ManualProvider = () => ({
74707
74744
  id: () => "manual",
74708
74745
  supportsLocal: () => false,
74709
74746
  install(cfg, _global, opts = {}) {
74710
- const url2 = mcpEndpoint3(cfg);
74747
+ const url2 = mcpEndpoint(cfg);
74711
74748
  const apiKey = mcpHeaders(cfg.active_account?.target?.api_key)["X-Dosu-API-Key"];
74712
74749
  const headerValue = opts.showSecret ? apiKey : maskSecret(apiKey);
74713
74750
  console.log("Use these details to configure the Dosu MCP server in your client:");
@@ -74727,7 +74764,6 @@ To remove the Dosu MCP server, manually delete the configuration from your clien
74727
74764
  }
74728
74765
  });
74729
74766
  var init_manual = __esm(() => {
74730
- init_config();
74731
74767
  init_config_helpers();
74732
74768
  });
74733
74769
 
@@ -74743,13 +74779,6 @@ function resolveGlobalConfigPath() {
74743
74779
  return jsoncPath;
74744
74780
  return jsonPath;
74745
74781
  }
74746
- function mcpEndpoint4(cfg) {
74747
- if (cfg.mode === MODE_OSS)
74748
- return mcpBaseURL();
74749
- if (!cfg.active_account?.target?.deployment_id)
74750
- throw new Error("deployment ID is required");
74751
- return mcpURL(cfg.active_account?.target?.deployment_id);
74752
- }
74753
74782
  var MCPorterProvider = () => ({
74754
74783
  name: () => "MCPorter",
74755
74784
  id: () => "mcporter",
@@ -74763,7 +74792,7 @@ var MCPorterProvider = () => ({
74763
74792
  const configPath2 = global2 ? resolveGlobalConfigPath() : join32(process.cwd(), "config", "mcporter.json");
74764
74793
  const server = {
74765
74794
  type: "http",
74766
- url: mcpEndpoint4(cfg),
74795
+ url: mcpEndpoint(cfg),
74767
74796
  headers: mcpHeaders(cfg.active_account.target.api_key)
74768
74797
  };
74769
74798
  installJSONServer(configPath2, "mcpServers", server);
@@ -74774,7 +74803,6 @@ var MCPorterProvider = () => ({
74774
74803
  }
74775
74804
  });
74776
74805
  var init_mcporter = __esm(() => {
74777
- init_config();
74778
74806
  init_config_helpers();
74779
74807
  init_detect();
74780
74808
  });
@@ -74789,16 +74817,15 @@ var OpenCodeProvider = () => createJSONProvider({
74789
74817
  paths: ["~/.config/opencode"],
74790
74818
  globalPath: "~/.config/opencode/opencode.json",
74791
74819
  topKey: "mcp",
74792
- buildServer: (cfg) => ({
74820
+ buildServer: ({ url: url2, headers }) => ({
74793
74821
  type: "remote",
74794
- url: mcpURL(cfg.active_account.target.deployment_id),
74822
+ url: url2,
74795
74823
  enabled: true,
74796
- headers: mcpHeaders(cfg.active_account.target.api_key)
74824
+ headers
74797
74825
  }),
74798
74826
  localConfigPath: (cwd) => join33(cwd, "opencode.json")
74799
74827
  });
74800
74828
  var init_opencode = __esm(() => {
74801
- init_config_helpers();
74802
74829
  init_base();
74803
74830
  });
74804
74831
 
@@ -74836,30 +74863,73 @@ var init_windsurf = __esm(() => {
74836
74863
  });
74837
74864
 
74838
74865
  // src/mcp/providers/zed.ts
74839
- 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";
74840
74868
  import { join as join36 } from "node:path";
74841
74869
  function zedConfigDir() {
74842
74870
  const os3 = platform3();
74843
- if (os3 === "darwin" || os3 === "win32")
74871
+ if (os3 === "win32")
74844
74872
  return join36(appSupportDir(), "Zed");
74873
+ if (os3 === "darwin")
74874
+ return join36(homedir9(), ".config", "zed");
74845
74875
  return join36(appSupportDir(), "zed");
74846
74876
  }
74847
- var ZedProvider = () => createJSONProvider({
74848
- providerName: "Zed",
74849
- providerID: "zed",
74850
- local: true,
74851
- priorityValue: 10,
74852
- paths: [zedConfigDir()],
74853
- globalPath: join36(zedConfigDir(), "settings.json"),
74854
- topKey: "context_servers",
74855
- buildServer: (cfg) => ({
74856
- source: "custom",
74857
- type: "http",
74858
- url: mcpURL(cfg.active_account.target.deployment_id),
74859
- headers: mcpHeaders(cfg.active_account.target.api_key)
74860
- }),
74861
- localConfigPath: (cwd) => join36(cwd, ".zed", "settings.json")
74862
- });
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
+ };
74863
74933
  var init_zed = __esm(() => {
74864
74934
  init_config_helpers();
74865
74935
  init_detect();
@@ -75073,13 +75143,13 @@ var init_update_check = __esm(() => {
75073
75143
 
75074
75144
  // src/rules/installer.ts
75075
75145
  import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync22, unlinkSync as unlinkSync4, writeFileSync as writeFileSync14 } from "node:fs";
75076
- import { homedir as homedir9 } from "node:os";
75146
+ import { homedir as homedir10 } from "node:os";
75077
75147
  import { dirname as dirname9, join as join38 } from "node:path";
75078
75148
  function claudeConfigDir() {
75079
- return process.env.CLAUDE_CONFIG_DIR || join38(homedir9(), ".claude");
75149
+ return process.env.CLAUDE_CONFIG_DIR || join38(homedir10(), ".claude");
75080
75150
  }
75081
75151
  function codexHome3() {
75082
- return process.env.CODEX_HOME || join38(homedir9(), ".codex");
75152
+ return process.env.CODEX_HOME || join38(homedir10(), ".codex");
75083
75153
  }
75084
75154
  function isRuleAgent(agent) {
75085
75155
  return Object.hasOwn(RULE_TARGETS, agent);
@@ -75225,7 +75295,7 @@ var init_installer = __esm(() => {
75225
75295
  },
75226
75296
  cursor: {
75227
75297
  kind: "file",
75228
- path: () => join38(homedir9(), ".cursor", "rules", "dosu.mdc"),
75298
+ path: () => join38(homedir10(), ".cursor", "rules", "dosu.mdc"),
75229
75299
  cursorFrontmatter: true
75230
75300
  },
75231
75301
  codex: {
@@ -75234,15 +75304,15 @@ var init_installer = __esm(() => {
75234
75304
  },
75235
75305
  opencode: {
75236
75306
  kind: "section",
75237
- path: () => join38(homedir9(), ".config", "opencode", "AGENTS.md")
75307
+ path: () => join38(homedir10(), ".config", "opencode", "AGENTS.md")
75238
75308
  },
75239
75309
  gemini: {
75240
75310
  kind: "section",
75241
- path: () => join38(homedir9(), ".gemini", "GEMINI.md")
75311
+ path: () => join38(homedir10(), ".gemini", "GEMINI.md")
75242
75312
  },
75243
75313
  antigravity: {
75244
75314
  kind: "section",
75245
- path: () => join38(homedir9(), ".gemini", "GEMINI.md")
75315
+ path: () => join38(homedir10(), ".gemini", "GEMINI.md")
75246
75316
  }
75247
75317
  };
75248
75318
  });
@@ -75380,6 +75450,140 @@ var init_errors3 = __esm(() => {
75380
75450
  // src/mcp/constants.ts
75381
75451
  var MCP_PROVIDER_SLUG2 = "dosu_mcp";
75382
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
+
75383
75587
  // src/tui/alt-screen.ts
75384
75588
  function enterAltScreen(output2) {
75385
75589
  depth += 1;
@@ -75403,7 +75607,8 @@ var init_alt_screen = __esm(() => {
75403
75607
  });
75404
75608
 
75405
75609
  // src/tui/activity-view.ts
75406
- 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";
75407
75612
  function reduceActivityViewKey(key) {
75408
75613
  if (key === "q" || key === ESC7 || key === CTRL_C4)
75409
75614
  return "back";
@@ -75417,6 +75622,10 @@ function reduceActivityViewKey(key) {
75417
75622
  return "down";
75418
75623
  if (key === "s")
75419
75624
  return "sync";
75625
+ if (key === "f")
75626
+ return "full";
75627
+ if (key === "c")
75628
+ return "clear";
75420
75629
  return "none";
75421
75630
  }
75422
75631
  function cycleTab(tab, delta = 1) {
@@ -75439,33 +75648,49 @@ function syncConfirmMode(status) {
75439
75648
  return "resume";
75440
75649
  return "start";
75441
75650
  }
75442
- function formatActivityLine(line, width) {
75651
+ function formatActivityLine(line, width, full = false) {
75443
75652
  const compact = stripAnsiCodes(line).replace(/^\[(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})[^\]]*\]/, (_match, _date2, time3) => time3);
75444
- if (compact.length <= width)
75653
+ if (full || compact.length <= width)
75445
75654
  return compact;
75446
75655
  return `${compact.slice(0, Math.max(0, width - 1))}…`;
75447
75656
  }
75448
75657
  function appendSyncActivity(buffer, chunk, max = ACTIVITY_VIEW_BUFFER_LINES) {
75449
75658
  const fresh = chunk.split(`
75450
- `).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)\]/, ""));
75451
75660
  return [...buffer, ...fresh].slice(-max);
75452
75661
  }
75453
75662
  function clip(text, max) {
75454
75663
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
75455
75664
  }
75456
- function formatMinedRow(record2) {
75665
+ function formatStudiedRow(record2, full = false, name) {
75457
75666
  const match = record2.at.match(/^\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2})/);
75458
75667
  const stamp = match ? `${match[1]} ${match[2]}` : record2.at;
75459
75668
  const slash = record2.session.indexOf("/");
75460
75669
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
75461
75670
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
75462
- const project = clip(record2.project ?? "-", 28);
75463
- 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}`;
75464
75675
  }
75465
- function formatQueuedRow(session) {
75676
+ function formatQueuedRow(session, full = false, name) {
75466
75677
  const stamp = session.updated.replace("T", " ").slice(5, 16);
75467
- const project = clip(session.project ?? "-", 28);
75468
- 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;
75469
75694
  }
75470
75695
  function windowList(lines, scroll, height = ACTIVITY_VIEW_LIST_LINES) {
75471
75696
  const clamped = Math.max(0, Math.min(scroll, Math.max(0, lines.length - height)));
@@ -75493,23 +75718,23 @@ function foldRunProgress(progress, chunk) {
75493
75718
  let current = progress;
75494
75719
  for (const line of chunk.split(`
75495
75720
  `)) {
75496
- const start = line.match(/\[sync\] mining (\d+) of \d+ ready sessions/);
75721
+ const start = line.match(/\[sync\] (?:studying|mining) (\d+) of \d+ ready sessions/);
75497
75722
  if (start) {
75498
75723
  current = { batch: Number.parseInt(start[1], 10), read: new Set, notes: 0 };
75499
75724
  continue;
75500
75725
  }
75501
- 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)) {
75502
75727
  current = null;
75503
75728
  continue;
75504
75729
  }
75505
75730
  if (!current)
75506
75731
  continue;
75507
- const read = line.match(/\[miner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75732
+ const read = line.match(/\[learner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75508
75733
  if (read) {
75509
75734
  current.read.add(read[1]);
75510
75735
  continue;
75511
75736
  }
75512
- if (line.includes("[miner] [agent] → mcp__dosu__write_knowledge")) {
75737
+ if (line.includes("[learner] [agent] → mcp__dosu__write_knowledge")) {
75513
75738
  current.notes += 1;
75514
75739
  }
75515
75740
  }
@@ -75523,15 +75748,15 @@ function statusLine(status) {
75523
75748
  if (status.running) {
75524
75749
  const since = status.startedAt ? ` · since ${localTime(status.startedAt)}` : "";
75525
75750
  const pid = status.pid !== undefined ? ` (pid ${status.pid})` : "";
75526
- 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}`)}`;
75527
75752
  }
75528
75753
  if (status.state.paused) {
75529
- 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")}`;
75530
75755
  }
75531
75756
  if (status.staleLock) {
75532
75757
  return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Not running")} ${import_picocolors30.default.dim("· a previous run exited without cleaning up")}`;
75533
75758
  }
75534
- 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")}`;
75535
75760
  }
75536
75761
  function progressLine(done, ready, width, notes = 0) {
75537
75762
  const total = done + ready;
@@ -75543,14 +75768,14 @@ function progressLine(done, ready, width, notes = 0) {
75543
75768
  const bar = brand("█".repeat(filled)) + import_picocolors30.default.dim("░".repeat(cells - filled));
75544
75769
  const pct = Math.floor(ratio * 100);
75545
75770
  const suffix = notes > 0 ? ` · ${notes} suggested page${notes === 1 ? "" : "s"}` : "";
75546
- return `${bar} ${done}/${total} mined · ${pct}%${suffix}`;
75771
+ return `${bar} ${done}/${total} studied · ${pct}%${suffix}`;
75547
75772
  }
75548
- function tabBar(tab, queuedCount, openCount, minedCount, width) {
75773
+ function tabBar(tab, queuedCount, openCount, studiedCount, width) {
75549
75774
  return tabStrip([
75550
- ["activity", "Activity"],
75551
- ["mined", `Mined (${minedCount})`],
75552
- ["queued", `Queued (${queuedCount})`],
75553
- ["open", `Open (${openCount})`]
75775
+ ["activity", "activity"],
75776
+ ["studied", `studied (${studiedCount})`],
75777
+ ["queued", `queued (${queuedCount})`],
75778
+ ["open", `open (${openCount})`]
75554
75779
  ], tab, width);
75555
75780
  }
75556
75781
  function activityWidth(columns) {
@@ -75572,10 +75797,11 @@ function wrapLine(text, width, indent = " ") {
75572
75797
  return out;
75573
75798
  }
75574
75799
  function confirmBox(queuedCount, backlog, width, mode = "start") {
75575
- const inFlight = backlog && backlog.inFlight > 0 ? ` (+${backlog.inFlight} open, mined once ${backlog.inFlight === 1 ? "it goes" : "they go"} quiet)` : "";
75576
- 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`;
75577
- const title = mode === "stop" ? "Stop mining?" : mode === "resume" ? "Resume mining?" : "Start mining now?";
75578
- 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";
75579
75805
  const maxInner = Math.max(20, Math.min(width, contentWidth()) - 4);
75580
75806
  const rows = [
75581
75807
  import_picocolors30.default.bold(brand(title)),
@@ -75591,64 +75817,143 @@ function confirmBox(queuedCount, backlog, width, mode = "start") {
75591
75817
  import_picocolors30.default.dim(`╰${"─".repeat(inner + 2)}╯`)
75592
75818
  ];
75593
75819
  }
75594
- function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], minedBeforeRun = 0, open5 = [], runProgress = null) {
75595
- 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";
75596
75822
  const queueDetail = [];
75597
75823
  if (status.backoffUntil) {
75598
75824
  queueDetail.push(`retrying after ${localTime(status.backoffUntil)} · s syncs now`);
75599
75825
  }
75600
75826
  let progress = null;
75601
75827
  if (status.running && backlog) {
75602
- const minedDelta = Math.max(0, (status.state.total_mined ?? 0) - minedBeforeRun);
75828
+ const studiedDelta = Math.max(0, (status.state.total_mined ?? 0) - studiedBeforeRun);
75603
75829
  const stepDone = runProgress ? Math.min(Math.max(0, runProgress.read.size - 1), runProgress.batch - 1, backlog.ready) : 0;
75604
- 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);
75605
75831
  }
75606
75832
  const refusal = !status.running && status.state.last_refusal;
75607
- const refusalLines = refusal ? wrapLine(`! Mining paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75608
- const minedRows = (status.state.mined_sessions ?? []).map((record2) => formatActivityLine(formatMinedRow(record2), width));
75609
- const queuedRows = queued.map((session) => formatActivityLine(formatQueuedRow(session), width));
75610
- const openRows = open5.map((session) => formatActivityLine(formatQueuedRow(session), width));
75611
- const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : minedRows;
75612
- const emptyMined = status.state.watermark ? "No sessions recorded yet. History starts with the next mining run." : "No mined sessions yet.";
75613
- 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;
75614
- const { visible, above, below } = windowList(source, pane.scroll);
75615
- 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)];
75616
75859
  const scrollParts = [];
75617
75860
  if (above > 0)
75618
75861
  scrollParts.push(`↑ ${above} earlier`);
75619
75862
  if (below > 0)
75620
75863
  scrollParts.push(`↓ ${below} newer`);
75621
- if (pane.tab !== "activity" && visible.length > 0) {
75622
- scrollParts.push(`full rows: dosu knowledge sessions --${pane.tab}`);
75623
- }
75624
75864
  const lines = [
75625
- breadcrumb(["Home", "Activity"], width),
75865
+ breadcrumb(["home", "activity"], width),
75626
75866
  "",
75627
75867
  statusLine(status),
75628
- import_picocolors30.default.dim(mined),
75868
+ import_picocolors30.default.dim(studied),
75629
75869
  ...queueDetail.length > 0 ? wrapLine(queueDetail.join(" · "), width).map((line) => import_picocolors30.default.dim(line)) : [],
75630
75870
  ...progress ? [progress] : [],
75631
75871
  ...refusalLines,
75632
75872
  "",
75633
- ...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),
75634
75874
  ...listRows,
75635
- ...scrollParts.length > 0 ? [import_picocolors30.default.dim(scrollParts.join(" · "))] : [],
75875
+ ...scrollParts.length > 0 ? [import_picocolors30.default.dim(clip(scrollParts.join(" · "), width))] : [],
75636
75876
  "",
75637
75877
  ...pane.confirm ? centerBlock(confirmBox(queued.length, backlog, width, pane.confirm), width) : [
75638
- import_picocolors30.default.dim([
75878
+ ...wrapLine([
75639
75879
  "tab switch",
75640
75880
  "↑↓ scroll",
75881
+ fullRows ? "f clip" : "f full rows",
75641
75882
  status.running ? "s stop" : status.state.paused ? "s resume" : "s sync now",
75883
+ ...!status.running && status.state.watermark ? ["c clear"] : [],
75642
75884
  "esc back"
75643
- ].join(" · "))
75885
+ ].join(" · "), width, "").map((line) => import_picocolors30.default.dim(line))
75644
75886
  ]
75645
75887
  ];
75646
75888
  return lines.join(`
75647
75889
  `);
75648
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
+ }
75649
75954
  function defaultReadLog2() {
75650
75955
  try {
75651
- return readFileSync24(logger.getLogPath(), "utf-8");
75956
+ return readFileSync25(logger.getLogPath(), "utf-8");
75652
75957
  } catch {
75653
75958
  return "";
75654
75959
  }
@@ -75664,7 +75969,9 @@ function runActivityView(io2 = {}) {
75664
75969
  const startSync = io2.startSync ?? (() => spawnDetachedSelf(["knowledge", "sync", "--bootstrap"]));
75665
75970
  const stopSync = io2.stopSync ?? stopSyncRun;
75666
75971
  const setPaused = io2.setPaused ?? setSyncPaused;
75972
+ const clearHistory = io2.clearHistory ?? resetSyncState;
75667
75973
  const listBacklog = io2.listBacklog ?? listSessionBacklog;
75974
+ const rowNamesFor = io2.rowNames ?? createRowNamer();
75668
75975
  const pollMs = io2.pollMs ?? ACTIVITY_VIEW_POLL_MS;
75669
75976
  const seed = readLog();
75670
75977
  let activity = appendSyncActivity([], seed);
@@ -75672,6 +75979,7 @@ function runActivityView(io2 = {}) {
75672
75979
  let runProgress = foldRunProgress(null, seed);
75673
75980
  let tab = "activity";
75674
75981
  let scroll = 0;
75982
+ let fullRows = false;
75675
75983
  let confirmSync = null;
75676
75984
  let status = getStatus();
75677
75985
  let sessions = listBacklog();
@@ -75690,30 +75998,32 @@ function runActivityView(io2 = {}) {
75690
75998
  return sessions.open.length;
75691
75999
  return (status.state.mined_sessions ?? []).length;
75692
76000
  };
76001
+ const listHeight = () => fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75693
76002
  let lastFrame = null;
75694
- let minedBeforeRun = null;
76003
+ let studiedBeforeRun = null;
75695
76004
  const draw = () => {
75696
76005
  status = getStatus();
75697
76006
  if (status.running) {
75698
76007
  const run2 = status.state.run;
75699
76008
  if (run2 && run2.pid === status.pid) {
75700
- minedBeforeRun = run2.baseline_mined;
76009
+ studiedBeforeRun = run2.baseline_mined;
75701
76010
  } else {
75702
- minedBeforeRun ??= status.state.total_mined ?? 0;
76011
+ studiedBeforeRun ??= status.state.total_mined ?? 0;
75703
76012
  }
75704
76013
  } else {
75705
- minedBeforeRun = null;
76014
+ studiedBeforeRun = null;
75706
76015
  }
75707
76016
  if (status.state.watermark !== queuedWatermark) {
75708
76017
  queuedWatermark = status.state.watermark;
75709
76018
  sessions = listBacklog();
75710
76019
  }
75711
- if ((confirmSync === "start" || confirmSync === "resume") && status.running)
76020
+ if (confirmSync !== null && confirmSync !== "stop" && status.running)
75712
76021
  confirmSync = null;
75713
76022
  if (confirmSync === "stop" && !status.running)
75714
76023
  confirmSync = null;
75715
76024
  const width = activityWidth(output2.columns ?? 80);
75716
- 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);
75717
76027
  if (frame === lastFrame)
75718
76028
  return;
75719
76029
  lastFrame = frame;
@@ -75760,11 +76070,15 @@ ${CLEAR_BELOW}`);
75760
76070
  const mode = confirmSync;
75761
76071
  confirmSync = null;
75762
76072
  let note;
75763
- 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") {
75764
76078
  const ok2 = status.pid !== undefined && stopSync(status.pid);
75765
76079
  if (ok2)
75766
76080
  setPaused(true);
75767
- 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";
75768
76082
  } else {
75769
76083
  if (mode === "resume")
75770
76084
  setPaused(false);
@@ -75788,14 +76102,23 @@ ${CLEAR_BELOW}`);
75788
76102
  if (action === "sync") {
75789
76103
  confirmSync = syncConfirmMode(status);
75790
76104
  draw();
76105
+ } else if (action === "clear") {
76106
+ if (!status.running && status.state.watermark) {
76107
+ confirmSync = "clear";
76108
+ draw();
76109
+ }
75791
76110
  } else if (action === "tab" || action === "tab-back") {
75792
76111
  tab = cycleTab(tab, action === "tab" ? 1 : -1);
75793
76112
  scroll = 0;
75794
76113
  if (tab === "queued" || tab === "open")
75795
76114
  sessions = listBacklog();
75796
76115
  draw();
76116
+ } else if (action === "full") {
76117
+ fullRows = !fullRows;
76118
+ scroll = 0;
76119
+ draw();
75797
76120
  } else if (action === "up") {
75798
- const maxScroll = Math.max(0, activeListLength() - ACTIVITY_VIEW_LIST_LINES);
76121
+ const maxScroll = Math.max(0, activeListLength() - listHeight());
75799
76122
  if (scroll < maxScroll) {
75800
76123
  scroll += 1;
75801
76124
  draw();
@@ -75811,10 +76134,12 @@ ${CLEAR_BELOW}`);
75811
76134
  input2.on("data", onData);
75812
76135
  });
75813
76136
  }
75814
- 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;
75815
76138
  var init_activity_view = __esm(() => {
75816
76139
  init_follow();
75817
76140
  init_logger();
76141
+ init_project_dir();
76142
+ init_session_title();
75818
76143
  init_styles();
75819
76144
  init_backlog();
75820
76145
  init_detach();
@@ -75836,7 +76161,7 @@ var init_activity_view = __esm(() => {
75836
76161
  CURSOR_HOME = `${ESC7}[H`;
75837
76162
  CLEAR_BELOW = `${ESC7}[0J`;
75838
76163
  CLEAR_EOL = `${ESC7}[K`;
75839
- ACTIVITY_VIEW_TABS = ["activity", "mined", "queued", "open"];
76164
+ ACTIVITY_VIEW_TABS = ["activity", "studied", "queued", "open"];
75840
76165
  DEFAULT_PANE = { tab: "activity", scroll: 0 };
75841
76166
  });
75842
76167
 
@@ -75849,7 +76174,7 @@ async function trackCliOnboardingEvent(cfg, onboardingRunID, event, properties =
75849
76174
  return;
75850
76175
  if (!cfg.active_account?.session.access_token)
75851
76176
  return;
75852
- if (!isUUID2(onboardingRunID))
76177
+ if (!isUUID3(onboardingRunID))
75853
76178
  return;
75854
76179
  const input2 = {
75855
76180
  event,
@@ -75872,7 +76197,7 @@ async function trackCliOnboardingPreAuthEvent(onboardingRunID, event, properties
75872
76197
  return;
75873
76198
  if (!analyticsReleaseEnabled())
75874
76199
  return;
75875
- if (!isUUID2(onboardingRunID))
76200
+ if (!isUUID3(onboardingRunID))
75876
76201
  return;
75877
76202
  const input2 = {
75878
76203
  event,
@@ -75905,13 +76230,14 @@ function analyticsReleaseEnabled() {
75905
76230
  return Boolean(parsePostHogProjectToken(process.env.DOSU_POSTHOG_PROJECT_TOKEN_OVERRIDE ?? "phc_Uo9RxhN0HRk8gZkPlT18BCr7eqsM0MPWVW88HZB95V5"));
75906
76231
  }
75907
76232
  function baseProperties(cfg) {
76233
+ const orgId = cfg.active_account?.target?.org_id;
75908
76234
  return {
75909
76235
  cli_version: safeIdentifier(VERSION, 32),
75910
76236
  install_channel: safeIdentifier(INSTALL_CHANNEL, 32),
75911
76237
  platform: safeIdentifier(process.platform, 24),
75912
76238
  arch: safeIdentifier(process.arch, 24),
75913
76239
  mode: cfg.mode ?? "cloud",
75914
- org_id: safeIdentifier(cfg.active_account?.target?.org_id, 128),
76240
+ ...orgId && isUUID3(orgId) ? { org_id: orgId, $groups: { organization: orgId } } : {},
75915
76241
  deployment_id: safeIdentifier(cfg.active_account?.target?.deployment_id, 128),
75916
76242
  space_id: safeIdentifier(cfg.active_account?.target?.space_id, 128)
75917
76243
  };
@@ -75946,7 +76272,7 @@ function allowlistedWorkflowProperties(properties) {
75946
76272
  function safeIdentifier(value, maxLength) {
75947
76273
  return typeof value === "string" && value.length <= maxLength && /^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(value) ? value : undefined;
75948
76274
  }
75949
- function isUUID2(value) {
76275
+ function isUUID3(value) {
75950
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);
75951
76277
  }
75952
76278
  function createAnalyticsClient(accessToken, requestSignal) {
@@ -76669,23 +76995,31 @@ async function stepOfferInitialSync(cfg) {
76669
76995
  s.start("Checking for recent agent sessions...");
76670
76996
  const outcome = await runKnowledgeSync({ bootstrap: true });
76671
76997
  if (outcome.status !== "backlog" || outcome.readySessions === 0) {
76672
- 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.");
76673
76999
  return;
76674
77000
  }
76675
77001
  const n5 = outcome.readySessions;
76676
- 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
+ `));
76677
77011
  const mineNow = await confirm2({
76678
- message: `Mine ${n5 === 1 ? "it" : "them"} for team knowledge now? (runs in the background)`,
76679
- active: "Mine now ⛏️",
76680
- inactive: "Skip",
77012
+ message: `Study ${them} now?`,
77013
+ active: "Study now \uD83D\uDCDA",
77014
+ inactive: "Skip for now",
76681
77015
  initialValue: true
76682
77016
  });
76683
77017
  if (isCancel2(mineNow) || !mineNow) {
76684
- 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.`);
76685
77019
  return;
76686
77020
  }
76687
77021
  if (spawnDetachedSelf(["knowledge", "sync", "--quiet", "--bootstrap"])) {
76688
- 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.`);
76689
77023
  const watch = await confirm2({
76690
77024
  message: "What next?",
76691
77025
  active: "Watch it work",
@@ -77510,12 +77844,15 @@ function loadPageStatsFromConfig() {
77510
77844
  }
77511
77845
  }
77512
77846
  function overviewRows(state) {
77513
- const minedTotal = state.total_mined ?? 0;
77847
+ const studiedTotal = state.total_mined ?? 0;
77514
77848
  const notes = state.total_notes ?? 0;
77515
77849
  const tokens = state.total_learning_tokens ?? 0;
77516
- if (minedTotal === 0 && notes === 0)
77850
+ if (studiedTotal === 0 && notes === 0)
77517
77851
  return [];
77518
- const rows = [`${label("Sessions mined")}${minedTotal}`, `${label("Suggested pages")}${notes}`];
77852
+ const rows = [
77853
+ `${label("Sessions studied")}${studiedTotal}`,
77854
+ `${label("Suggested pages")}${notes}`
77855
+ ];
77519
77856
  if (tokens > 0) {
77520
77857
  rows.push(`${label("Investigation distilled")}${formatTokenCount(tokens)} tokens`);
77521
77858
  }
@@ -77564,7 +77901,7 @@ function analyticsTabRows(tab, state, pageStats) {
77564
77901
  function renderAnalyticsFrame(state, tab, scroll, pageStats = null, pagesPending = false, width = contentWidth()) {
77565
77902
  const rows = analyticsTabRows(tab, state, pageStats);
77566
77903
  const { visible, above, below } = windowReport(rows, scroll);
77567
- 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.";
77568
77905
  const listRows = visible.length > 0 ? visible.map((row) => import_picocolors32.default.dim(row)) : [import_picocolors32.default.dim(empty)];
77569
77906
  const scrollParts = [];
77570
77907
  if (above > 0)
@@ -77572,12 +77909,12 @@ function renderAnalyticsFrame(state, tab, scroll, pageStats = null, pagesPending
77572
77909
  if (below > 0)
77573
77910
  scrollParts.push(`↓ ${below} more`);
77574
77911
  const lines = [
77575
- breadcrumb(["Home", "Analytics"], width),
77912
+ breadcrumb(["home", "analytics"], width),
77576
77913
  "",
77577
77914
  ...tabStrip([
77578
- ["overview", "Overview"],
77579
- ["projects", "Projects"],
77580
- ["pages", "Pages"]
77915
+ ["overview", "overview"],
77916
+ ["projects", "projects"],
77917
+ ["pages", "pages"]
77581
77918
  ], tab, width, { spread: false }),
77582
77919
  "",
77583
77920
  ...listRows,
@@ -77716,7 +78053,7 @@ function checklistRows(ctx) {
77716
78053
  const rows = [["workspace", ctx.directory]];
77717
78054
  rows.push([
77718
78055
  "account",
77719
- 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`)}`
77720
78057
  ]);
77721
78058
  const missing = new Set(ctx.setupMissing ?? []);
77722
78059
  const warnRow = `${off} ${import_picocolors33.default.yellow("not configured")} ${import_picocolors33.default.dim(`${DOT3} run Setup`)}`;
@@ -77740,10 +78077,10 @@ function checklistRows(ctx) {
77740
78077
  rows.push(["agents", `${on2} ${ctx.agents.join(` ${DOT3} `)}`]);
77741
78078
  if (missing.has("hooks"))
77742
78079
  rows.push(["hooks", warnRow]);
77743
- if (ctx.mining) {
78080
+ if (ctx.studying) {
77744
78081
  rows.push([
77745
78082
  "sync",
77746
- `⛏️ ${brand("mining sessions...")} ${import_picocolors33.default.dim(`${DOT3} see Activity`)}`
78083
+ `\uD83D\uDCDA ${brand("studying sessions...")} ${import_picocolors33.default.dim(`${DOT3} see activity`)}`
77747
78084
  ]);
77748
78085
  }
77749
78086
  if (ctx.update) {
@@ -77920,7 +78257,7 @@ function renderPagesFrame(model, width, selected) {
77920
78257
  }
77921
78258
  const count = model.phase === "ready" ? ` ${import_picocolors34.default.dim(model.pages.length < model.total ? `(${model.pages.length} of ${model.total})` : `(${model.pages.length})`)}` : "";
77922
78259
  const lines = [
77923
- `${breadcrumb(["Home", "Pages"], width)}${count}`,
78260
+ `${breadcrumb(["home", "pages"], width)}${count}`,
77924
78261
  "",
77925
78262
  ...body,
77926
78263
  ...scrollHint ? [import_picocolors34.default.dim(scrollHint)] : [],
@@ -77950,7 +78287,7 @@ function renderPageFrame(page, content, width, scroll) {
77950
78287
  }
77951
78288
  const meta3 = `${page.type} · ${page.published ? "published" : "draft"} · updated ${page.updated_at.slice(0, 10)}`;
77952
78289
  const lines = [
77953
- breadcrumb(["Home", "Pages", page.title], width),
78290
+ breadcrumb(["home", "pages", page.title], width),
77954
78291
  import_picocolors34.default.dim(clip3(meta3, width)),
77955
78292
  "",
77956
78293
  ...body,
@@ -78133,9 +78470,19 @@ __export(exports_tui, {
78133
78470
  runTUI: () => runTUI,
78134
78471
  handleLogout: () => handleLogout
78135
78472
  });
78136
- import { homedir as homedir10 } from "node:os";
78137
- import { basename as basename4 } from "node:path";
78138
- 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) {
78139
78486
  let webAppHost = "app.dosu.dev";
78140
78487
  try {
78141
78488
  webAppHost = new URL(getWebAppURL()).host;
@@ -78151,14 +78498,15 @@ function bannerContext(cfg) {
78151
78498
  return {
78152
78499
  version: getVersionString(),
78153
78500
  webAppHost,
78154
- directory: basename4(process.cwd()),
78501
+ directory: basename6(process.cwd()),
78155
78502
  signedIn: isAuthenticated(cfg),
78503
+ sessionExpired: session.expired,
78156
78504
  deploymentName: cfg.active_account?.target?.deployment_name,
78157
78505
  libraryName: cfg.active_account?.target?.library_name,
78158
- setupMissing: isAuthenticated(cfg) ? missingSetupSteps(cfg) : [],
78506
+ setupMissing: isAuthenticated(cfg) && !session.expired ? missingSetupSteps(cfg) : [],
78159
78507
  ...inGitWorkTree() ? { repoAgentsMd: dosuAgentsSectionState() } : {},
78160
78508
  agents,
78161
- mining: isMining(),
78509
+ studying: isStudying(),
78162
78510
  ...latest ? { update: { version: latest, hint: buildUpdateHint(INSTALL_CHANNEL, isNpxInvocation()) } } : {}
78163
78511
  };
78164
78512
  }
@@ -78225,24 +78573,25 @@ function setupHint(cfg) {
78225
78573
  const started = Boolean(cfg.active_account?.target);
78226
78574
  return import_picocolors35.default.yellow(started ? `incomplete · missing ${missingSetupSteps(cfg).join(" + ")}` : "not set up yet");
78227
78575
  }
78228
- function isMining() {
78576
+ function isStudying() {
78229
78577
  try {
78230
78578
  return getSyncStatus({ readLog: () => "" }).running;
78231
78579
  } catch {
78232
78580
  return false;
78233
78581
  }
78234
78582
  }
78235
- function drawHome(cfg) {
78583
+ function drawHome(cfg, session) {
78236
78584
  if (process.stdout.isTTY) {
78237
78585
  process.stdout.write(CLEAR_SCREEN);
78238
78586
  process.stdout.write(`
78239
78587
  `.repeat(frameTopMargin()));
78240
78588
  }
78241
- process.stdout.write(`${renderBanner(bannerContext(cfg))}
78589
+ process.stdout.write(`${renderBanner(bannerContext(cfg, session))}
78242
78590
  `);
78243
78591
  }
78244
78592
  async function runMainMenu() {
78245
78593
  const cfg = loadConfig();
78594
+ const session = await probeSession(cfg);
78246
78595
  const runSetupAndReload = async () => {
78247
78596
  await runSetup();
78248
78597
  const fresh = loadConfig();
@@ -78250,34 +78599,44 @@ async function runMainMenu() {
78250
78599
  cfg.active_account = fresh.active_account;
78251
78600
  };
78252
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
+ }
78253
78612
  if (!isAuthenticated(cfg)) {
78254
78613
  return [
78255
- { label: "Log in / Sign up", hint: "(opens your browser)", value: "auth" },
78256
- { label: "Exit", value: "exit" }
78614
+ { label: "log in / sign up", hint: "(opens your browser)", value: "auth" },
78615
+ { label: "exit", value: "exit" }
78257
78616
  ];
78258
78617
  }
78259
78618
  if (!isSetUp(cfg)) {
78260
78619
  return [
78261
- { label: "Setup", hint: setupHint(cfg), value: "setup" },
78262
- { label: "Exit", value: "exit" }
78620
+ { label: "setup", hint: setupHint(cfg), value: "setup" },
78621
+ { label: "exit", value: "exit" }
78263
78622
  ];
78264
78623
  }
78265
- const mining = isMining();
78624
+ const studying = isStudying();
78266
78625
  return [
78267
78626
  {
78268
- label: mining ? `Activity ⛏️ ${brand("mining sessions...")}` : "Activity",
78627
+ label: studying ? `activity \uD83D\uDCDA ${brand("studying sessions...")}` : "activity",
78269
78628
  value: "sync"
78270
78629
  },
78271
- { label: "Knowledge report", hint: "(opens in browser)", value: "report" },
78272
- { label: "Analytics", value: "analytics" },
78273
- { label: "Pages", value: "pages" },
78274
- { label: "Settings", value: "settings" },
78275
- { 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" }
78276
78635
  ];
78277
78636
  };
78278
- const home = () => drawHome(cfg);
78637
+ const home = () => drawHome(cfg, session);
78279
78638
  home();
78280
- if (isAuthenticated(cfg) && !isSetUp(cfg)) {
78639
+ if (isAuthenticated(cfg) && !session.expired && !isSetUp(cfg)) {
78281
78640
  await runSetupAndReload();
78282
78641
  home();
78283
78642
  }
@@ -78311,6 +78670,7 @@ async function runMainMenu() {
78311
78670
  break;
78312
78671
  case "auth":
78313
78672
  await handleAuthenticate(cfg);
78673
+ session.expired = session.expired && isTokenExpired(cfg);
78314
78674
  home();
78315
78675
  if (isAuthenticated(cfg) && !isSetUp(cfg)) {
78316
78676
  await runSetupAndReload();
@@ -78340,19 +78700,19 @@ async function runSettings(cfg) {
78340
78700
  const target = cfg.active_account?.target;
78341
78701
  const library = target?.library_name ?? target?.deployment_name ?? "not configured";
78342
78702
  const filter = loadSyncState().project_filter;
78343
- const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename4(dir)).join(", ") : `${filter.length} projects`;
78344
- const action = await menuSelect("Settings", [
78345
- { label: "Switch organization", hint: target?.org_name, value: "switch-org" },
78346
- { label: "Switch Library", hint: library, value: "switch-library" },
78347
- { label: "Mining scope", hint: scope, value: "projects" },
78348
- { label: "Run setup", hint: "rerun the setup wizard", value: "setup" },
78349
- { label: "Log out", hint: "clear stored credentials", value: "logout" },
78350
- { 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" }
78351
78711
  ]);
78352
78712
  if (action === null || action === "back")
78353
78713
  return;
78354
78714
  if (action === "projects") {
78355
- await runMiningProjectsSetting();
78715
+ await runStudyingProjectsSetting();
78356
78716
  continue;
78357
78717
  }
78358
78718
  if (action === "setup") {
@@ -78375,7 +78735,7 @@ async function runSettings(cfg) {
78375
78735
  }
78376
78736
  }
78377
78737
  function displayDir(dir) {
78378
- const home = homedir10();
78738
+ const home = homedir12();
78379
78739
  return dir === home ? "~" : dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir;
78380
78740
  }
78381
78741
  function discoverProjectDirs() {
@@ -78390,7 +78750,7 @@ function discoverProjectDirs() {
78390
78750
  } catch {}
78391
78751
  return [...counts.entries()].sort((a9, b2) => b2[1] - a9[1]).map(([dir]) => dir);
78392
78752
  }
78393
- async function runMiningProjectsSetting() {
78753
+ async function runStudyingProjectsSetting() {
78394
78754
  const dirs = discoverProjectDirs();
78395
78755
  if (dirs.length === 0) {
78396
78756
  log2.info("No local agent sessions found yet; nothing to scope.");
@@ -78398,7 +78758,7 @@ async function runMiningProjectsSetting() {
78398
78758
  }
78399
78759
  const current = loadSyncState().project_filter;
78400
78760
  const selected = await multiselect2({
78401
- message: "Mine sessions from which folders?",
78761
+ message: "Study sessions from which folders?",
78402
78762
  options: dirs.map((dir) => ({
78403
78763
  label: dir === UNKNOWN_PROJECT ? "(unknown folder)" : displayDir(dir),
78404
78764
  value: dir
@@ -78413,7 +78773,7 @@ async function runMiningProjectsSetting() {
78413
78773
  const all = selected.length === dirs.length;
78414
78774
  saveSyncState(all ? state : { ...state, project_filter: [...selected] });
78415
78775
  const scope = all ? "all folders" : selected.map(displayDir).join(", ");
78416
- log2.success(`Mining scope ${dim(`· ${scope}`)}`);
78776
+ log2.success(`Study scope ${dim(`· ${scope}`)}`);
78417
78777
  }
78418
78778
  async function handleAuthenticate(cfg) {
78419
78779
  const shouldLogin = await confirm2({ message: "Open browser to log in?" });
@@ -78459,6 +78819,7 @@ function handleLogout(cfg) {
78459
78819
  }
78460
78820
  var import_picocolors35, ESC10, CLEAR_SCREEN;
78461
78821
  var init_tui = __esm(() => {
78822
+ init_client();
78462
78823
  init_config();
78463
78824
  init_agents();
78464
78825
  init_providers();
@@ -79116,7 +79477,7 @@ import {
79116
79477
  constants as constants3,
79117
79478
  fstatSync,
79118
79479
  openSync as openSync5,
79119
- readFileSync as readFileSync25,
79480
+ readFileSync as readFileSync26,
79120
79481
  readSync as readSync4,
79121
79482
  unlinkSync as unlinkSync5
79122
79483
  } from "node:fs";
@@ -82318,23 +82679,23 @@ ${results.length - opts.limit} more results not shown.`));
82318
82679
  console.log(` ID: ${store.id}`);
82319
82680
  console.log(` Space ID: ${store.space_id}`);
82320
82681
  });
82321
- 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) => {
82322
- 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;
82323
82684
  const wantQueued = all || Boolean(opts.queued);
82324
82685
  const wantOpen = all || Boolean(opts.open);
82325
- const wantMined = all || Boolean(opts.mined);
82686
+ const wantStudied = all || Boolean(opts.studied);
82326
82687
  const backlog = wantQueued || wantOpen ? listSessionBacklog() : { queued: [], open: [] };
82327
- const mined = wantMined ? loadSyncState().mined_sessions ?? [] : [];
82688
+ const studied = wantStudied ? loadSyncState().mined_sessions ?? [] : [];
82328
82689
  if (opts.json) {
82329
82690
  printResult({
82330
82691
  ...wantQueued ? { queued: backlog.queued } : {},
82331
82692
  ...wantOpen ? { open: backlog.open } : {},
82332
- ...wantMined ? { mined } : {}
82693
+ ...wantStudied ? { studied } : {}
82333
82694
  }, opts);
82334
82695
  return;
82335
82696
  }
82336
82697
  const sessionRows = (sessions) => sessions.map((s) => [s.harness, s.updated, s.project ?? "-", s.id]);
82337
- const minedRows = mined.map((record2) => {
82698
+ const studiedRows = studied.map((record2) => {
82338
82699
  const slash = record2.session.indexOf("/");
82339
82700
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
82340
82701
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
@@ -82358,11 +82719,11 @@ ${results.length - opts.limit} more results not shown.`));
82358
82719
  if (wantOpen) {
82359
82720
  section("Open", sessionRows(backlog.open), "Updated", "No open sessions. Live agent sessions sit here until they go quiet.");
82360
82721
  }
82361
- if (wantMined) {
82362
- section("Mined", minedRows, "Mined at", "No mined sessions recorded yet.");
82722
+ if (wantStudied) {
82723
+ section("Studied", studiedRows, "Studied at", "No studied sessions recorded yet.");
82363
82724
  }
82364
82725
  });
82365
- 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) => {
82366
82727
  if (opts.status) {
82367
82728
  const status = getSyncStatus();
82368
82729
  if (opts.json) {
@@ -82383,7 +82744,7 @@ ${results.length - opts.limit} more results not shown.`));
82383
82744
  ]);
82384
82745
  return;
82385
82746
  }
82386
- const deps = { mine: buildMiner(opts.quiet ? "hook" : "manual") };
82747
+ const deps = { mine: buildLearner(opts.quiet ? "hook" : "manual") };
82387
82748
  let outcome = await runKnowledgeSync({
82388
82749
  quiet: opts.quiet,
82389
82750
  bootstrap: opts.bootstrap,
@@ -82391,7 +82752,7 @@ ${results.length - opts.limit} more results not shown.`));
82391
82752
  });
82392
82753
  if (opts.bootstrap && deps.mine) {
82393
82754
  const maxRounds = Math.ceil(outcome.readySessions / MINE_BATCH_LIMIT) + 2;
82394
- for (let round = 1;outcome.status === "mined" && round < maxRounds; round++) {
82755
+ for (let round = 1;outcome.status === "studied" && round < maxRounds; round++) {
82395
82756
  if (!opts.quiet && !opts.json)
82396
82757
  printSyncOutcome(outcome);
82397
82758
  outcome = await runKnowledgeSync({ quiet: opts.quiet, bootstrap: true, deps });
@@ -82448,12 +82809,12 @@ ${results.length - opts.limit} more results not shown.`));
82448
82809
  console.log("All your notes already have a transcript — nothing to backfill.");
82449
82810
  return;
82450
82811
  }
82451
- 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.`);
82452
82813
  });
82453
82814
  cmd.addCommand(hooksCommand());
82454
82815
  return cmd;
82455
82816
  }
82456
- function buildMiner(trigger) {
82817
+ function buildLearner(trigger) {
82457
82818
  const cfg = loadConfig();
82458
82819
  if (cfg.mode === "oss")
82459
82820
  return;
@@ -82462,8 +82823,8 @@ function buildMiner(trigger) {
82462
82823
  return;
82463
82824
  const { api_key, deployment_id } = target;
82464
82825
  return async (sessions) => {
82465
- const { runMiner: runMiner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82466
- 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 });
82467
82828
  };
82468
82829
  }
82469
82830
  function formatAge(iso, now) {
@@ -82489,14 +82850,14 @@ function printSyncStatus(status, now = new Date) {
82489
82850
  console.log("○ No sync running.");
82490
82851
  }
82491
82852
  if (status.state.paused) {
82492
- 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'."));
82493
82854
  }
82494
82855
  const wm2 = status.state.watermark;
82495
- 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"}`);
82496
82857
  if (status.state.project_filter?.length) {
82497
82858
  const home = homedir5();
82498
82859
  const scope = status.state.project_filter.map((dir) => dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir).join(", ");
82499
- console.log(` Mining scope: ${scope}`);
82860
+ console.log(` Study scope: ${scope}`);
82500
82861
  }
82501
82862
  if ((status.state.total_notes ?? 0) > 0) {
82502
82863
  const tokens = status.state.total_learning_tokens ?? 0;
@@ -82511,7 +82872,7 @@ function printSyncStatus(status, now = new Date) {
82511
82872
  console.log(import_picocolors18.default.yellow(` Backing off after ${n5} failure${n5 === 1 ? "" : "s"}; background syncs retry after ${status.backoffUntil}.`));
82512
82873
  }
82513
82874
  if (status.state.last_refusal) {
82514
- 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)})`));
82515
82876
  }
82516
82877
  if (status.recentActivity.length > 0) {
82517
82878
  console.log(`
@@ -82527,25 +82888,25 @@ function printSyncOutcome(outcome) {
82527
82888
  switch (outcome.status) {
82528
82889
  case "backlog": {
82529
82890
  const inFlight = outcome.inFlightSessions > 0 ? import_picocolors18.default.dim(` (${outcome.inFlightSessions} more still in progress)`) : "";
82530
- console.log(`✓ Scanned. ${outcome.readySessions} new session${outcome.readySessions === 1 ? "" : "s"} ready to mine${inFlight}.`);
82531
- 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."));
82532
82893
  break;
82533
82894
  }
82534
- case "mined": {
82535
- const notes = outcome.miner?.notesWritten ?? 0;
82536
- const remaining = outcome.readySessions - (outcome.minedSessions ?? 0);
82537
- 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.`);
82538
82899
  if (remaining > 0) {
82539
82900
  console.log(import_picocolors18.default.dim(`${remaining} more in the backlog; run sync again to continue.`));
82540
82901
  }
82541
82902
  break;
82542
82903
  }
82543
82904
  case "skipped-gateway": {
82544
- 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."));
82545
82906
  break;
82546
82907
  }
82547
82908
  case "mine-failed": {
82548
- 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."));
82549
82910
  process.exitCode = 1;
82550
82911
  break;
82551
82912
  }
@@ -82568,7 +82929,7 @@ function printSyncOutcome(outcome) {
82568
82929
  break;
82569
82930
  }
82570
82931
  case "skipped-paused": {
82571
- 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."));
82572
82933
  break;
82573
82934
  }
82574
82935
  }
@@ -83320,7 +83681,10 @@ init_skill();
83320
83681
  // src/commands/sources.ts
83321
83682
  init_commander();
83322
83683
  init_trpc();
83684
+ init_github_step();
83685
+ init_installation_server();
83323
83686
  var import_picocolors25 = __toESM(require_picocolors(), 1);
83687
+ var CONNECT_TIMEOUT_DEFAULT_SECONDS = 600;
83324
83688
  function requireConfig11() {
83325
83689
  const cfg = requireLoginConfig();
83326
83690
  if (!cfg.active_account?.target?.org_id) {
@@ -83329,6 +83693,15 @@ function requireConfig11() {
83329
83693
  }
83330
83694
  return cfg;
83331
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
+ }
83332
83705
  function sourcesCommand() {
83333
83706
  const cmd = new Command("sources").description("Manage connected data sources");
83334
83707
  cmd.command("list").description("List all connected data sources").option("--json", "Output as JSON").action(async (opts) => {
@@ -83411,6 +83784,154 @@ function sourcesCommand() {
83411
83784
  }
83412
83785
  console.log(import_picocolors25.default.green("Data source deleted."));
83413
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
+ });
83414
83935
  return cmd;
83415
83936
  }
83416
83937
 
@@ -83924,10 +84445,12 @@ function commandTelemetryContext() {
83924
84445
  const tokenUserID = getAccessTokenUserID(accessToken);
83925
84446
  const userID = configUserID && configUserID === tokenUserID ? configUserID : undefined;
83926
84447
  const email3 = userID ? getAccessTokenEmail(accessToken) : undefined;
84448
+ const orgId = userID ? cfg.active_account?.target?.org_id : undefined;
83927
84449
  return {
83928
84450
  mode: cfg.mode === MODE_OSS ? "oss" : "cloud",
83929
84451
  isAuthenticated: authenticated,
83930
- ...userID ? { user: { id: userID, ...email3 ? { email: email3 } : {} } } : {}
84452
+ ...userID ? { user: { id: userID, ...email3 ? { email: email3 } : {} } } : {},
84453
+ ...orgId ? { orgId } : {}
83931
84454
  };
83932
84455
  } catch {
83933
84456
  return { mode: "cloud", isAuthenticated: false };
@@ -84326,7 +84849,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84326
84849
  }
84327
84850
  if (opts.follow) {
84328
84851
  try {
84329
- const lines = readFileSync25(logPath, "utf-8").split(`
84852
+ const lines = readFileSync26(logPath, "utf-8").split(`
84330
84853
  `);
84331
84854
  if (lines.at(-1) === "")
84332
84855
  lines.pop();
@@ -84342,7 +84865,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84342
84865
  if (opts.tail !== undefined) {
84343
84866
  const n5 = typeof opts.tail === "string" ? parseInt(opts.tail, 10) || 50 : 50;
84344
84867
  try {
84345
- const content = readFileSync25(logPath, "utf-8");
84868
+ const content = readFileSync26(logPath, "utf-8");
84346
84869
  const lines = content.split(`
84347
84870
  `);
84348
84871
  console.log(lines.slice(-n5).join(`
@@ -84413,4 +84936,4 @@ execute().catch((err) => {
84413
84936
  process.exit(1);
84414
84937
  });
84415
84938
 
84416
- //# debugId=cf222ab5-3069-6407-6475-6e2164756e21
84939
+ //# debugId=e98d2bf5-99f8-402d-6475-6e2164756e21