@dosu/cli 0.62.0 → 0.63.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 +2 -1
  2. package/bin/dosu.js +327 -218
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -182,7 +182,8 @@ dosu knowledge incognito enable|disable [claude|cursor|codex] # the /dosu-incog
182
182
  ```
183
183
 
184
184
  The status line shows one of `📚 Dosu studying…`, `👻 Dosu incognito`, `⚪ Dosu paused`,
185
- `⚪ Dosu not studying this folder`, or `⚪ Dosu off`. Neither setup nor `enable` replaces a status
185
+ `⚪ Dosu not studying this repo`, or `⚪ Dosu off`. Only sessions inside a git repo are studied;
186
+ pick which repos under `dosu` → settings → study scope. Neither setup nor `enable` replaces a status
186
187
  line you already have; they print the one-liner to add to your own script instead.
187
188
 
188
189
  Running `/dosu-incognito` inside a session marks that session's transcript so studying skips it
package/bin/dosu.js CHANGED
@@ -1083,7 +1083,7 @@ function isNpxInvocation(channel = INSTALL_CHANNEL, env = process.env) {
1083
1083
  function getVersionString() {
1084
1084
  return `v${VERSION}`;
1085
1085
  }
1086
- var VERSION = "0.62.0", INSTALL_CHANNEL = "npm";
1086
+ var VERSION = "0.63.0", INSTALL_CHANNEL = "npm";
1087
1087
 
1088
1088
  // src/debug/logger.ts
1089
1089
  import {
@@ -1472,6 +1472,59 @@ var init_agents = __esm(() => {
1472
1472
  init_formats();
1473
1473
  });
1474
1474
 
1475
+ // src/sessions/repo.ts
1476
+ import { execFileSync } from "node:child_process";
1477
+ function normalizeRepoRemote(remote) {
1478
+ const value = remote.trim();
1479
+ if (!value)
1480
+ return null;
1481
+ let host;
1482
+ let path;
1483
+ const scp = value.includes("://") ? null : /^(?:[^@/]+@)?([^:/]+):(.+)$/.exec(value);
1484
+ if (scp) {
1485
+ host = scp[1];
1486
+ path = scp[2];
1487
+ } else {
1488
+ try {
1489
+ const url = new URL(value);
1490
+ if (url.protocol === "file:")
1491
+ return null;
1492
+ host = url.hostname;
1493
+ path = url.pathname;
1494
+ } catch {
1495
+ return null;
1496
+ }
1497
+ }
1498
+ path = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
1499
+ if (!host || !path.includes("/"))
1500
+ return null;
1501
+ const lowerHost = host.toLowerCase();
1502
+ return `${lowerHost}/${lowerHost === "github.com" ? path.toLowerCase() : path}`;
1503
+ }
1504
+ function originRepoOfDir(dir) {
1505
+ const env = { ...process.env };
1506
+ for (const name of GIT_LOCATION_ENV)
1507
+ delete env[name];
1508
+ try {
1509
+ const out = execFileSync("git", ["-C", dir, "remote", "get-url", "origin"], {
1510
+ encoding: "utf-8",
1511
+ env,
1512
+ stdio: ["ignore", "pipe", "ignore"],
1513
+ timeout: 1000
1514
+ });
1515
+ return normalizeRepoRemote(out);
1516
+ } catch {
1517
+ return null;
1518
+ }
1519
+ }
1520
+ function displayRepo(repo) {
1521
+ return repo.split("/").slice(1).join("/") || repo;
1522
+ }
1523
+ var GIT_LOCATION_ENV;
1524
+ var init_repo = __esm(() => {
1525
+ GIT_LOCATION_ENV = ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"];
1526
+ });
1527
+
1475
1528
  // src/sessions/scan.ts
1476
1529
  import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "node:fs";
1477
1530
  import { createRequire as createRequire2 } from "node:module";
@@ -1919,6 +1972,9 @@ import { join as join6 } from "node:path";
1919
1972
  function syncStatePath(configDir = getConfigDir()) {
1920
1973
  return join6(configDir, STATE_FILENAME);
1921
1974
  }
1975
+ function stringsOf(values) {
1976
+ return values.filter((v) => typeof v === "string");
1977
+ }
1922
1978
  function loadSyncState(configDir = getConfigDir()) {
1923
1979
  const empty = {
1924
1980
  schema_version: STATE_SCHEMA_VERSION,
@@ -1954,9 +2010,8 @@ function loadSyncState(configDir = getConfigDir()) {
1954
2010
  ...lastRefusal ? { last_refusal: lastRefusal } : {},
1955
2011
  ...run ? { run } : {},
1956
2012
  ...raw.paused === true ? { paused: true } : {},
1957
- ...Array.isArray(raw.project_filter) ? {
1958
- project_filter: raw.project_filter.filter((p) => typeof p === "string")
1959
- } : {}
2013
+ ...Array.isArray(raw.repo_filter) ? { repo_filter: stringsOf(raw.repo_filter) } : {},
2014
+ ...Array.isArray(raw.project_filter) ? { project_filter: stringsOf(raw.project_filter) } : {}
1960
2015
  };
1961
2016
  } catch {
1962
2017
  return empty;
@@ -1976,6 +2031,7 @@ function resetSyncState(configDir = getConfigDir()) {
1976
2031
  schema_version: STATE_SCHEMA_VERSION,
1977
2032
  watermark: null,
1978
2033
  consecutive_failures: 0,
2034
+ ...previous.repo_filter ? { repo_filter: previous.repo_filter } : {},
1979
2035
  ...previous.project_filter ? { project_filter: previous.project_filter } : {},
1980
2036
  ...previous.paused ? { paused: true } : {}
1981
2037
  };
@@ -2002,17 +2058,31 @@ function isUnderDir(dir, base) {
2002
2058
  const root = base.endsWith("/") ? base.slice(0, -1) : base;
2003
2059
  return dir === root || dir.startsWith(`${root}/`);
2004
2060
  }
2005
- function filterSessionsByProject(sessions, filter, resolveDir) {
2006
- if (!filter || filter.length === 0)
2007
- return [...sessions];
2008
- const includeUnknown = filter.includes(UNKNOWN_PROJECT);
2009
- const dirs = filter.filter((entry) => entry !== UNKNOWN_PROJECT);
2010
- return sessions.filter((session) => {
2011
- const dir = resolveDir(session);
2012
- if (!dir)
2013
- return includeUnknown;
2014
- return dirs.some((base) => isUnderDir(dir, base));
2015
- });
2061
+ function studyRepoFilter(state, listSessions, locator) {
2062
+ if (state.repo_filter)
2063
+ return state.repo_filter;
2064
+ const folders = state.project_filter;
2065
+ if (!folders?.length)
2066
+ return null;
2067
+ const repos = new Set;
2068
+ for (const session of listSessions()) {
2069
+ const dir = locator.resolve(session);
2070
+ if (!dir || !folders.some((base) => isUnderDir(dir, base)))
2071
+ continue;
2072
+ const repo = locator.resolveRepo(session);
2073
+ if (repo)
2074
+ repos.add(repo);
2075
+ }
2076
+ return [...repos].sort();
2077
+ }
2078
+ function filterSessionsByRepo(sessions, filter, resolveRepo) {
2079
+ const kept = [];
2080
+ for (const session of sessions) {
2081
+ const repo = resolveRepo(session);
2082
+ if (repo !== null && (filter === null || filter.includes(repo)))
2083
+ kept.push({ ...session, repo });
2084
+ }
2085
+ return kept;
2016
2086
  }
2017
2087
  function gateSessions(sessions, watermark, now = new Date, quietPeriodMs = DEFAULT_QUIET_PERIOD_MS) {
2018
2088
  const watermarkMs = watermark ? Date.parse(watermark) : Number.NEGATIVE_INFINITY;
@@ -2031,7 +2101,7 @@ function gateSessions(sessions, watermark, now = new Date, quietPeriodMs = DEFAU
2031
2101
  }
2032
2102
  return { ready, open };
2033
2103
  }
2034
- 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)";
2104
+ var STATE_FILENAME = "knowledge-sync.json", STATE_SCHEMA_VERSION = 1, DEFAULT_QUIET_PERIOD_MS, BACKOFF_BASE_MS, BACKOFF_MAX_MS, STUDIED_HISTORY_LIMIT = 500;
2035
2105
  var init_watermark = __esm(() => {
2036
2106
  init_config();
2037
2107
  DEFAULT_QUIET_PERIOD_MS = 5 * 60 * 1000;
@@ -2067,12 +2137,18 @@ function defaultHookEnabled(agentId) {
2067
2137
  return false;
2068
2138
  }
2069
2139
  }
2070
- function cwdIsStudied(cwd, filter) {
2071
- if (!filter || filter.length === 0)
2072
- return true;
2140
+ function cwdIsStudied(cwd, state, repoOfDir) {
2073
2141
  if (!cwd)
2074
- return filter.includes(UNKNOWN_PROJECT);
2075
- return filter.some((base) => base !== UNKNOWN_PROJECT && isUnderDir(cwd, base));
2142
+ return false;
2143
+ const repo = repoOfDir(cwd);
2144
+ if (!repo)
2145
+ return false;
2146
+ if (state.repo_filter)
2147
+ return state.repo_filter.includes(repo);
2148
+ if (state.project_filter?.length) {
2149
+ return state.project_filter.some((base) => isUnderDir(cwd, base));
2150
+ }
2151
+ return true;
2076
2152
  }
2077
2153
  function resolveStatuslineState(payload, agentId, deps = {}) {
2078
2154
  const hookEnabled = deps.hookEnabled ?? defaultHookEnabled;
@@ -2085,7 +2161,7 @@ function resolveStatuslineState(payload, agentId, deps = {}) {
2085
2161
  const state = (deps.loadState ?? loadSyncState)();
2086
2162
  if (state.paused)
2087
2163
  return "paused";
2088
- if (!cwdIsStudied(payload.cwd, state.project_filter))
2164
+ if (!cwdIsStudied(payload.cwd, state, deps.repoOfDir ?? originRepoOfDir))
2089
2165
  return "not-studied";
2090
2166
  return "on";
2091
2167
  }
@@ -2099,13 +2175,14 @@ function renderStatusline(raw, agentId, deps = {}) {
2099
2175
  var STATUSLINE_LABELS;
2100
2176
  var init_render = __esm(() => {
2101
2177
  init_agents();
2178
+ init_repo();
2102
2179
  init_incognito();
2103
2180
  init_watermark();
2104
2181
  STATUSLINE_LABELS = {
2105
2182
  on: "\uD83D\uDCDA Dosu studying…",
2106
2183
  incognito: "\uD83D\uDC7B Dosu incognito",
2107
2184
  paused: "⚪ Dosu paused",
2108
- "not-studied": "⚪ Dosu not studying this folder",
2185
+ "not-studied": "⚪ Dosu not studying this repo",
2109
2186
  off: "⚪ Dosu off"
2110
2187
  };
2111
2188
  });
@@ -9863,7 +9940,7 @@ var init_default_browser_id = __esm(() => {
9863
9940
  // node_modules/run-applescript/index.js
9864
9941
  import process6 from "node:process";
9865
9942
  import { promisify as promisify4 } from "node:util";
9866
- import { execFile as execFile4, execFileSync } from "node:child_process";
9943
+ import { execFile as execFile4, execFileSync as execFileSync2 } from "node:child_process";
9867
9944
  async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
9868
9945
  if (process6.platform !== "darwin") {
9869
9946
  throw new Error("macOS only");
@@ -11195,8 +11272,8 @@ async function resolveDeploymentId(client, activeOrgId, id) {
11195
11272
  process.exit(1);
11196
11273
  }
11197
11274
  function deploymentsCommand() {
11198
- const cmd = new Command("deployments").description("Manage deployments");
11199
- cmd.command("list").description("List all deployments").option("--json", "Output as JSON").action(async (opts) => {
11275
+ const cmd = new Command("deployments").description("Select the MCP deployment (and its Library) this CLI uses; not for Agents");
11276
+ cmd.command("list").description("List MCP deployments you can select").option("--json", "Output as JSON").action(async (opts) => {
11200
11277
  const cfg = requireConfig4();
11201
11278
  const client = createTypedClient(cfg);
11202
11279
  const allDeployments = await listAccessibleDeployments(client, cfg.active_account?.target?.org_id);
@@ -11206,7 +11283,7 @@ function deploymentsCommand() {
11206
11283
  return;
11207
11284
  }
11208
11285
  if (!deployments || deployments.length === 0) {
11209
- console.log(import_picocolors15.default.dim("No deployments found."));
11286
+ console.log(import_picocolors15.default.dim("No MCP deployments found."));
11210
11287
  return;
11211
11288
  }
11212
11289
  printTable(["ID", "Name", "Org", "Status"], deployments.map((d) => [
@@ -11220,7 +11297,7 @@ function deploymentsCommand() {
11220
11297
  ${import_picocolors15.default.dim(`Current: ${cfg.active_account?.target?.deployment_name ?? cfg.active_account?.target?.deployment_id}`)}`);
11221
11298
  }
11222
11299
  });
11223
- cmd.command("info").description("Show current deployment details").option("--json", "Output as JSON").action(async (opts) => {
11300
+ cmd.command("info").description("Show the selected MCP deployment").option("--json", "Output as JSON").action(async (opts) => {
11224
11301
  const cfg = requireConfig4();
11225
11302
  if (!cfg.active_account?.target?.deployment_id) {
11226
11303
  console.error(import_picocolors15.default.red("No deployment selected. Run 'dosu setup' or 'dosu deployments switch'."));
@@ -11247,7 +11324,7 @@ ${import_picocolors15.default.dim(`Current: ${cfg.active_account?.target?.deploy
11247
11324
  ["Created", formatDate(deployment.created_at)]
11248
11325
  ], { rawData: deployment });
11249
11326
  });
11250
- cmd.command("switch").description("Switch to a different deployment").argument("<id>", "Deployment ID (full UUID, or a unique prefix as shown by 'list')").option("--json", "Output as JSON").action(async (rawId, opts) => {
11327
+ cmd.command("switch").description("Select a different MCP deployment (mints a new API key for it)").argument("<id>", "MCP deployment ID (full UUID, or a unique prefix as shown by 'list')").option("--json", "Output as JSON").action(async (rawId, opts) => {
11251
11328
  const cfg = requireConfig4();
11252
11329
  const client = createTypedClient(cfg);
11253
11330
  const id = await resolveDeploymentId(client, cfg.active_account?.target?.org_id, rawId);
@@ -12000,20 +12077,39 @@ function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
12000
12077
  return null;
12001
12078
  }
12002
12079
  };
12080
+ const repoOfDir = deps.repoOfDir ?? originRepoOfDir;
12081
+ const repoByDir = new Map;
12082
+ const resolve = (session) => {
12083
+ const key = `${session.harness}/${session.id}`;
12084
+ const cached = entries[key];
12085
+ if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
12086
+ return cached.dir;
12087
+ }
12088
+ const dir = compute(session);
12089
+ entries[key] = { dir, mtime: mtime(session.path) };
12090
+ dirty = true;
12091
+ return dir;
12092
+ };
12003
12093
  return {
12004
12094
  cached(key) {
12005
12095
  return entries[key]?.dir ?? null;
12006
12096
  },
12007
- resolve(session) {
12008
- const key = `${session.harness}/${session.id}`;
12009
- const cached = entries[key];
12010
- if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
12011
- return cached.dir;
12012
- }
12013
- const dir = compute(session);
12014
- entries[key] = { dir, mtime: mtime(session.path) };
12097
+ resolve,
12098
+ resolveRepo(session) {
12099
+ const dir = resolve(session);
12100
+ if (dir === null)
12101
+ return null;
12102
+ const entry = entries[`${session.harness}/${session.id}`];
12103
+ if (entry.repo !== undefined && (entry.repo !== null || entry.mtime === mtime(session.path))) {
12104
+ return entry.repo;
12105
+ }
12106
+ if (!repoByDir.has(dir))
12107
+ repoByDir.set(dir, repoOfDir(dir));
12108
+ const repo = repoByDir.get(dir) ?? null;
12109
+ entry.repo = repo;
12110
+ entry.mtime = mtime(session.path);
12015
12111
  dirty = true;
12016
- return dir;
12112
+ return repo;
12017
12113
  },
12018
12114
  flush() {
12019
12115
  if (!dirty)
@@ -12026,6 +12122,7 @@ function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
12026
12122
  var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
12027
12123
  var init_project_dir = __esm(() => {
12028
12124
  init_config();
12125
+ init_repo();
12029
12126
  HEAD_BYTES = 128 * 1024;
12030
12127
  });
12031
12128
 
@@ -13309,12 +13406,11 @@ var init_generate = __esm(() => {
13309
13406
  function listSessionBacklog() {
13310
13407
  try {
13311
13408
  const state = loadSyncState();
13312
- let sessions = scanAgentSessions({});
13313
- if (state.project_filter?.length) {
13314
- const resolver = createProjectDirResolver();
13315
- sessions = filterSessionsByProject(sessions, state.project_filter, resolver.resolve);
13316
- resolver.flush();
13317
- }
13409
+ const scanned = scanAgentSessions({});
13410
+ const resolver = createProjectDirResolver();
13411
+ const filter = studyRepoFilter(state, () => scanned, resolver);
13412
+ const sessions = filterSessionsByRepo(scanned, filter, resolver.resolveRepo);
13413
+ resolver.flush();
13318
13414
  const gate = gateSessions(sessions, state.watermark);
13319
13415
  const { kept, skipped } = partitionIncognitoSessions(gate.ready);
13320
13416
  return { queued: kept.reverse(), open: gate.open.reverse(), incognito: skipped.reverse() };
@@ -13540,19 +13636,24 @@ async function runKnowledgeSync(options = {}) {
13540
13636
  since: new Date(now().getTime() - GATE_WINDOW_DAYS * 24 * 60 * 60 * 1000),
13541
13637
  limit: GATE_WINDOW
13542
13638
  }));
13543
- let sessions = await listSessions();
13544
- if (state.project_filter?.length) {
13545
- let resolve = deps.resolveProjectDir;
13546
- let flush;
13547
- if (!resolve) {
13548
- const resolver = createProjectDirResolver();
13549
- resolve = resolver.resolve;
13550
- flush = resolver.flush;
13551
- }
13552
- sessions = filterSessionsByProject(sessions, state.project_filter, resolve);
13553
- flush?.();
13554
- logger.debug("sync", `project filter active: ${state.project_filter.join(", ")}`);
13555
- }
13639
+ const scanned = await listSessions();
13640
+ let flush;
13641
+ let locator = deps.locator;
13642
+ if (!locator) {
13643
+ const resolver = createProjectDirResolver();
13644
+ locator = resolver;
13645
+ flush = resolver.flush;
13646
+ }
13647
+ const repoFilter = studyRepoFilter(state, () => scanAgentSessions({}), locator);
13648
+ if (state.project_filter) {
13649
+ delete state.project_filter;
13650
+ if (repoFilter)
13651
+ state.repo_filter = repoFilter;
13652
+ logger.debug("sync", `folder scope converted to repos: ${repoFilter?.join(", ") || "none"}`);
13653
+ }
13654
+ const sessions = filterSessionsByRepo(scanned, repoFilter, (s) => locator.resolveRepo(s));
13655
+ flush?.();
13656
+ logger.debug("sync", `study scope ${repoFilter ? repoFilter.join(", ") || "none" : "all repos"}: ${sessions.length} of ${scanned.length} sessions in scope`);
13556
13657
  ({ ready, open } = gateSessions(sessions, state.watermark, now()));
13557
13658
  logGateResult(ready, open.length, state.watermark);
13558
13659
  } catch (err) {
@@ -13800,7 +13901,7 @@ var init_conflicts = __esm(() => {
13800
13901
  });
13801
13902
 
13802
13903
  // src/telemetry/debug-id.ts
13803
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "da3b44a3-063b-fb2c-6475-6e2164756e21";
13904
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "01f74cf6-cdbb-b296-6475-6e2164756e21";
13804
13905
 
13805
13906
  // src/telemetry/telemetry.ts
13806
13907
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -15477,9 +15578,8 @@ note cap are the volume guards.
15477
15578
  5. Title is a noun-phrase topic (like "page_version UniqueViolation race"), not a sentence.
15478
15579
  Content is a self-contained observation in plain language; include file/path pointers when
15479
15580
  useful.
15480
- 6. Only pass repo/branch to write_knowledge when the session itself verifies them (an explicit
15481
- cwd, git remote, or branch mentioned in the transcript). Never infer or guess a repo. When not
15482
- verified, omit both.
15581
+ 6. Never pass repo or branch to write_knowledge; the client attaches each note's repo from the
15582
+ session it came from.
15483
15583
  7. Never quote credentials, tokens, or secrets — even redacted placeholders — and never include
15484
15584
  long verbatim transcript spans. Summarize in your own words.
15485
15585
  8. A trivial session with no real user query is normal: skip it silently.`;
@@ -58494,6 +58594,7 @@ async function runLearner(options) {
58494
58594
  let currentSession;
58495
58595
  const readsSinceWrite = new Set;
58496
58596
  const sessionKeys = new Map(options.sessions.map((s) => [s.id, `${s.harness}/${s.id}`]));
58597
+ const sessionRepos = new Map(options.sessions.map((s) => [s.id, s.repo]));
58497
58598
  const notedSessions = new Set;
58498
58599
  const pendingNotes = new Map;
58499
58600
  let turns = 0;
@@ -58577,10 +58678,20 @@ async function runLearner(options) {
58577
58678
  const noted = currentSession && sessionKeys.get(currentSession);
58578
58679
  if (noted)
58579
58680
  pendingNotes.set(toolUseID, noted);
58580
- const { transcript_id: _authoredByModel, ...clean } = input;
58681
+ const {
58682
+ transcript_id: _transcriptByModel,
58683
+ repo: _repoByModel,
58684
+ branch: _branchByModel,
58685
+ ...clean
58686
+ } = input;
58687
+ const repo = currentSession && sessionRepos.get(currentSession);
58581
58688
  return {
58582
58689
  behavior: "allow",
58583
- updatedInput: currentSession ? { ...clean, transcript_id: currentSession } : clean
58690
+ updatedInput: {
58691
+ ...clean,
58692
+ ...currentSession ? { transcript_id: currentSession } : {},
58693
+ ...repo ? { repo } : {}
58694
+ }
58584
58695
  };
58585
58696
  }
58586
58697
  return { behavior: "allow", updatedInput: input };
@@ -58670,7 +58781,6 @@ var init_runner = __esm(() => {
58670
58781
 
58671
58782
  // src/commands/knowledge.ts
58672
58783
  import { existsSync as existsSync18 } from "node:fs";
58673
- import { homedir as homedir7 } from "node:os";
58674
58784
  import { delimiter as delimiter4, join as join21 } from "node:path";
58675
58785
  function requireConfig7() {
58676
58786
  const cfg = requireLoginConfig();
@@ -58938,10 +59048,11 @@ function printSyncStatus(status, now = new Date) {
58938
59048
  }
58939
59049
  const wm = status.state.watermark;
58940
59050
  console.log(` Studied through: ${wm ? `${wm} (${formatAge(wm, now)})` : "nothing studied yet"}`);
58941
- if (status.state.project_filter?.length) {
58942
- const home = homedir7();
58943
- const scope = status.state.project_filter.map((dir) => dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir).join(", ");
58944
- console.log(` Study scope: ${scope}`);
59051
+ const repoFilter = status.state.repo_filter;
59052
+ if (repoFilter) {
59053
+ console.log(` Study scope: ${repoFilter.length ? repoFilter.map(displayRepo).join(", ") : "no repos"}`);
59054
+ } else if (status.state.project_filter?.length) {
59055
+ console.log(` Study scope: ${status.state.project_filter.length} folders (converted to repos on the next sync)`);
58945
59056
  }
58946
59057
  if ((status.state.total_notes ?? 0) > 0) {
58947
59058
  const tokens = status.state.total_learning_tokens ?? 0;
@@ -59104,6 +59215,7 @@ var init_knowledge = __esm(() => {
59104
59215
  init_agents();
59105
59216
  init_formats();
59106
59217
  init_generate();
59218
+ init_repo();
59107
59219
  init_backlog();
59108
59220
  init_detach();
59109
59221
  init_status();
@@ -59898,7 +60010,94 @@ var init_skills = __esm(() => {
59898
60010
  files: [
59899
60011
  {
59900
60012
  path: "SKILL.md",
59901
- content: "---\nname: dosu\ndescription: 'Use the Dosu CLI to authenticate and configure coding agents; manage Libraries, Agents, sources, Monitor, documents, reviews, threads, topics, members, integrations, and deployments; query knowledge; or audit a repository for Dosu-generated docs. Use for Dosu platform work that should be done from a terminal instead of the web app.'\n---\n\n# Use the Dosu CLI\n\nOperate Dosu through `dosu`. Prefer structured output and let the CLI and App validate product rules.\n\n## Start correctly\n\n1. Run `dosu --version`. Do not probe installation with unrelated package-manager commands.\n2. If it is unavailable and Node is present, try `npx -y @dosu/cli --version` and use `npx -y @dosu/cli` as the command prefix for this session. Otherwise ask before installing anything.\n3. Run `dosu --help` or the relevant nested `--help` before using a command not covered by [the command reference](references/commands.md).\n4. Pass `--json` whenever that leaf command exposes it. Parse the result; do not scrape the human table.\n\n## Satisfy the right prerequisite\n\n- `dosu login` supplies the JWT used by tRPC-backed commands.\n- `dosu setup` selects an organization, Library context, and MCP deployment, creates an API key, and can configure an agent tool.\n- `dosu ask` needs an API key. `docs generate`, `docs auto-tag`, and `docs publish` need both JWT login and the API key. Other platform commands generally need JWT login; commands scoped to an organization or current Library also need the corresponding setup selection.\n- For coding-agent setup, follow the nonblocking `dosu setup --agent --tool <id>` workflow in [workflows.md](references/workflows.md). Relay its URL and execute its returned `resume_command`; never invent ticket commands.\n\n## Use current product terms\n\n- **Organization** is the account and permission boundary.\n- **Library** is the knowledge container shown in the App. Older APIs may call it a `space`; do not present that internal name to users.\n- **Data source** is an organization-level connection such as a repository, Slack channel, or Notion workspace. A Library can use already-connected sources.\n- **Agent** is a configurable GitHub, GitLab, Slack, or Teams deployment serving exactly one Library.\n- **Monitor** is per Library and source. It supports GitHub, GitLab, and Azure DevOps sources.\n- `dosu deployments` manages selectable **MCP deployments**. Do not use it as an Agent CRUD command.\n\n## Route by intent\n\n| Intent | Route |\n|---|---|\n| Set up Dosu end-to-end (\"set up dosu\", new Library from scratch) | Read the guided onboarding flow in [workflows.md](references/workflows.md) first; it is checkpoint-driven — ask, wait, then act |\n| Create or configure a Library | `dosu libraries ...` |\n| Attach existing organization sources | `dosu sources list` → `dosu libraries sources attach` |\n| Create or configure an Agent | `dosu agents ...` |\n| Ask for a synthesized answer | `dosu ask` |\n| Find source documents | `dosu knowledge search`, then `dosu docs get` |\n| Create, import, or publish docs | `dosu docs ...` |\n| Review a pending doc change or draft reply | Read [review-workflow.md](references/review-workflow.md) first |\n| Inspect conversations | `dosu threads ...` |\n| Browse managed topics | `dosu topics ...` |\n| Audit agent docs, README, architecture, or dependencies | Read [audit.md](references/audit.md) first |\n\nUse [commands.md](references/commands.md) as the sole detailed command and flag reference. Use [workflows.md](references/workflows.md) only for multi-command composition.\n\n## Apply Library and Agent semantics\n\n- To assemble a Library, list organization sources, create the Library, attach the chosen source IDs, then verify with `libraries info` and `libraries sources list`. The CLI cannot establish a brand-new OAuth connection.\n- `libraries sources config` resolves the provider on the App side. Read the command reference before choosing provider-specific options.\n- When creating a Library with a GitHub repository, or newly attaching one to an existing Library, treat Monitor as part of that setup unless the user opts out. Enable it with the whole-repository and `emoji` defaults, verify it, and explain that Monitor reviews pull requests to keep the Library's knowledge up to date. Follow [the Library workflow](references/workflows.md); do not ask the user to choose defaults.\n- Create an Agent from an existing source ID and let the App choose its defaults; do not synthesize config in shell commands. Each data source can back at most one Agent: a `CONFLICT` (409) from `agents create` means the source already has one — offer a different source or an authorized `agents move` instead of retrying.\n- Read Agent config before changing one existing leaf. Values are JSON. If a concurrent write returns `CONFLICT`, read again, re-evaluate the requested change, and retry only if it is still correct.\n- Moving an Agent replaces its Library. Verify the returned `space_id`; do not infer migration behavior for historical data from the move receipt.\n\n## Respect write boundaries\n\n- Treat an explicit, unambiguous user request for the exact mutation as authorization. Otherwise show the intended change and wait for confirmation.\n- For commands exposing `--confirm`, omit it first when a preview is useful; `{ \"confirmRequired\": true, \"applied\": false }` means nothing changed. Pass `--confirm` only after authorization.\n- Never batch-approve or batch-reject review items. Diff one item, explain it, and decide only the ID the user authorized.\n- Before deleting a Library, Agent, source, or document, state the exact target and impact. Before detaching a source, read and state the impact documented in the command reference. Some older delete commands do not enforce `--confirm`; the absence of a CLI prompt is not user approval.\n- Before attaching a source to a public Library, warn that its content becomes available to everyone who can access that Library.\n- OAuth connection and billing remain web-only. Report the boundary instead of claiming success.\n\n## Handle failures literally\n\n- `Not logged in` or an unrecoverable expired session: run `dosu login` (agent setup may instead return a ticket flow).\n- Missing organization, Library, deployment, or API-key context: run the appropriate `dosu setup` flow.\n- `confirmRequired`: no write occurred.\n- `CONFLICT` on config: reread before retrying.\n- A tRPC or backend error is a failed operation. Surface the code/path/status and do not imply the requested state exists.\n\n## References\n\n- [commands.md](references/commands.md): authoritative CLI syntax, choices, defaults, conflicts, and prerequisites\n- [workflows.md](references/workflows.md): reusable multi-command flows\n- [review-workflow.md](references/review-workflow.md): review safety and context\n- [audit.md](references/audit.md): repository audit procedure\n- [audit-findings-schema.md](references/audit-findings-schema.md): `.dosu/audit.json` contract\n",
60013
+ content: `---
60014
+ name: dosu
60015
+ description: 'Use the Dosu CLI to show the Dosu review list or review queue (items pending approval: pending doc changes and draft replies) and to review, approve, or reject one item; authenticate and configure coding agents; manage Libraries, Agents, sources, Monitor, documents, threads, topics, members, integrations, and MCP deployments; query knowledge; or audit a repository for Dosu-generated docs. Use for Dosu platform work that should be done from a terminal instead of the web app.'
60016
+ ---
60017
+
60018
+ # Use the Dosu CLI
60019
+
60020
+ Operate Dosu through \`dosu\`. Prefer structured output and let the CLI and App validate product rules.
60021
+
60022
+ ## Start correctly
60023
+
60024
+ 1. Run \`dosu --version\`. Do not probe installation with unrelated package-manager commands.
60025
+ 2. If it is unavailable and Node is present, try \`npx -y @dosu/cli --version\` and use \`npx -y @dosu/cli\` as the command prefix for this session. Otherwise ask before installing anything.
60026
+ 3. Run \`dosu --help\` or the relevant nested \`--help\` before using a command not covered by [the command reference](references/commands.md).
60027
+ 4. Pass \`--json\` whenever that leaf command exposes it. Parse the result; do not scrape the human table.
60028
+
60029
+ ## Satisfy the right prerequisite
60030
+
60031
+ - \`dosu login\` supplies the JWT used by tRPC-backed commands.
60032
+ - \`dosu setup\` selects an organization, Library context, and MCP deployment, creates an API key, and can configure an agent tool.
60033
+ - \`dosu ask\` needs an API key. \`docs generate\`, \`docs auto-tag\`, and \`docs publish\` need both JWT login and the API key. Other platform commands generally need JWT login; commands scoped to an organization or current Library also need the corresponding setup selection.
60034
+ - For coding-agent setup, follow the nonblocking \`dosu setup --agent --tool <id>\` workflow in [workflows.md](references/workflows.md). Relay its URL and execute its returned \`resume_command\`; never invent ticket commands.
60035
+
60036
+ ## Use current product terms
60037
+
60038
+ - **Organization** is the account and permission boundary.
60039
+ - **Library** is the knowledge container shown in the App. Older APIs may call it a \`space\`; do not present that internal name to users.
60040
+ - **Data source** is an organization-level connection such as a repository, Slack channel, or Notion workspace. A Library can use already-connected sources.
60041
+ - **Agent** is a configurable GitHub, GitLab, Slack, or Teams deployment serving exactly one Library.
60042
+ - **Monitor** is per Library and source. It supports GitHub, GitLab, and Azure DevOps sources.
60043
+ - **MCP deployment** is the local selection made by \`dosu setup\` or \`dosu deployments switch\`. It sets the Library that Library-scoped commands such as \`dosu review list\` read, plus the API key \`dosu ask\` uses. \`dosu deployments\` lists and switches MCP deployments; it is not an Agent command.
60044
+ - The **review queue** (\`dosu review list\`) holds pending doc changes in the selected Library plus draft replies from the selected MCP deployment. The App's **Review** page is a list of conversation threads, so its entries and counts can differ. When the user explicitly means that page, explain the difference and offer \`dosu threads list\` or \`dosu review context <thread-id>\`; do not substitute an Agent list.
60045
+
60046
+ ## Route by intent
60047
+
60048
+ | Intent | Route |
60049
+ |---|---|
60050
+ | Set up Dosu end-to-end ("set up dosu", new Library from scratch) | Read the guided onboarding flow in [workflows.md](references/workflows.md) first; it is checkpoint-driven — ask, wait, then act |
60051
+ | Create or configure a Library | \`dosu libraries ...\` |
60052
+ | Attach existing organization sources | \`dosu sources list\` → \`dosu libraries sources attach\` |
60053
+ | Create or configure an Agent | \`dosu agents ...\` |
60054
+ | Ask for a synthesized answer | \`dosu ask\` |
60055
+ | Find source documents | \`dosu knowledge search\`, then \`dosu docs get\` |
60056
+ | Create, import, or publish docs | \`dosu docs ...\` |
60057
+ | Show the review list, review queue, or what needs approval | \`dosu review list --json\`; if the scope looks wrong, follow the context check in [review-workflow.md](references/review-workflow.md) |
60058
+ | Inspect, edit, approve, or reject one review item | Read [review-workflow.md](references/review-workflow.md) first |
60059
+ | Inspect conversations | \`dosu threads ...\` |
60060
+ | Browse managed topics | \`dosu topics ...\` |
60061
+ | Audit agent docs, README, architecture, or dependencies | Read [audit.md](references/audit.md) first |
60062
+
60063
+ Use [commands.md](references/commands.md) as the sole detailed command and flag reference. Use [workflows.md](references/workflows.md) only for multi-command composition.
60064
+
60065
+ ## Apply Library and Agent semantics
60066
+
60067
+ - To assemble a Library, list organization sources, create the Library, attach the chosen source IDs, then verify with \`libraries info\` and \`libraries sources list\`. The CLI cannot establish a brand-new OAuth connection.
60068
+ - \`libraries sources config\` resolves the provider on the App side. Read the command reference before choosing provider-specific options.
60069
+ - When creating a Library with a GitHub repository, or newly attaching one to an existing Library, treat Monitor as part of that setup unless the user opts out. Enable it with the whole-repository and \`emoji\` defaults, verify it, and explain that Monitor reviews pull requests to keep the Library's knowledge up to date. Follow [the Library workflow](references/workflows.md); do not ask the user to choose defaults.
60070
+ - Create an Agent from an existing source ID and let the App choose its defaults; do not synthesize config in shell commands. Each data source can back at most one Agent: a \`CONFLICT\` (409) from \`agents create\` means the source already has one — offer a different source or an authorized \`agents move\` instead of retrying.
60071
+ - Read Agent config before changing one existing leaf. Values are JSON. If a concurrent write returns \`CONFLICT\`, read again, re-evaluate the requested change, and retry only if it is still correct.
60072
+ - Moving an Agent replaces its Library. Verify the returned \`space_id\`; do not infer migration behavior for historical data from the move receipt.
60073
+
60074
+ ## Respect write boundaries
60075
+
60076
+ - Treat an explicit, unambiguous user request for the exact mutation as authorization. Otherwise show the intended change and wait for confirmation.
60077
+ - For commands exposing \`--confirm\`, omit it first when a preview is useful; \`{ "confirmRequired": true, "applied": false }\` means nothing changed. Pass \`--confirm\` only after authorization.
60078
+ - Never batch-approve or batch-reject review items. Diff one item, explain it, and decide only the ID the user authorized.
60079
+ - Before deleting a Library, Agent, source, or document, state the exact target and impact. Before detaching a source, read and state the impact documented in the command reference. Some older delete commands do not enforce \`--confirm\`; the absence of a CLI prompt is not user approval.
60080
+ - Before attaching a source to a public Library, warn that its content becomes available to everyone who can access that Library.
60081
+ - OAuth connection and billing remain web-only. Report the boundary instead of claiming success.
60082
+
60083
+ ## Handle failures literally
60084
+
60085
+ - \`Not logged in\` or an unrecoverable expired session: run \`dosu login\` (agent setup may instead return a ticket flow).
60086
+ - Missing organization, Library, deployment, or API-key context: run the appropriate \`dosu setup\` flow.
60087
+ - Wrong or unexpected Library context: read \`dosu status --json\` and \`dosu deployments info --json\` before changing anything. Switch only to an MCP deployment the user named or confirmed; never pick the first one listed, and never switch just because a list came back empty.
60088
+ - Skill guidance out of date: \`dosu skill update\` rewrites the installed skill from the running CLI version. Some agents read skills only when a session starts, so start a new session if the change does not appear. Installed files show the skill is on disk, not that the agent loaded it.
60089
+ - \`confirmRequired\`: no write occurred.
60090
+ - \`CONFLICT\` on config: reread before retrying.
60091
+ - A tRPC or backend error is a failed operation. Surface the code/path/status and do not imply the requested state exists.
60092
+
60093
+ ## References
60094
+
60095
+ - [commands.md](references/commands.md): authoritative CLI syntax, choices, defaults, conflicts, and prerequisites
60096
+ - [workflows.md](references/workflows.md): reusable multi-command flows
60097
+ - [review-workflow.md](references/review-workflow.md): review safety and context
60098
+ - [audit.md](references/audit.md): repository audit procedure
60099
+ - [audit-findings-schema.md](references/audit-findings-schema.md): \`.dosu/audit.json\` contract
60100
+ `,
59902
60101
  executable: false
59903
60102
  },
59904
60103
  {
@@ -60249,7 +60448,7 @@ dosu docs sync-back <id> [--json]
60249
60448
  Read [review-workflow.md](review-workflow.md) before mutating a review item.
60250
60449
 
60251
60450
  \`\`\`text
60252
- dosu review list [--json]
60451
+ dosu review list [--since <when>] [--until <when>] [--json]
60253
60452
  dosu review diff <id> [--json]
60254
60453
  dosu review edit <id> [--title <title>] [--body <markdown> | --body-file <path>] [--json]
60255
60454
  dosu review context <thread-id> [--json]
@@ -60258,7 +60457,9 @@ dosu review reject <id> [--confirm] [--json]
60258
60457
  dosu review revert <id> [--json]
60259
60458
  \`\`\`
60260
60459
 
60261
- \`edit\` requires at least one field. \`approve\` and \`reject\` do not write without interactive confirmation or \`--confirm\`.
60460
+ - \`list\` reads the selected Library's pending doc changes plus the selected MCP deployment's draft replies. JSON is \`{items, truncated, total}\`: at most 50 items, newest first; \`total\` is a lower bound when \`truncated\` is true. \`--since\`/\`--until\` take \`24h\`/\`7d\`/\`2w\`, a UTC date, or an ISO-8601 datetime.
60461
+ - IDs are opaque: pass each \`id\` from \`list\` verbatim (draft replies carry a \`draft_message:\` prefix). \`revert\` takes a decided doc change's page-version ID and does not apply to drafts.
60462
+ - \`edit\` requires at least one field; drafts accept only \`--body\`/\`--body-file\`. \`approve\` and \`reject\` do not write without interactive confirmation or \`--confirm\`.
60262
60463
 
60263
60464
  ## Sources, integrations, members, and organization
60264
60465
 
@@ -60345,98 +60546,7 @@ The coding agent writes the findings; Dosu generates the selected docs. Follow [
60345
60546
  },
60346
60547
  {
60347
60548
  path: "references/review-workflow.md",
60348
- content: `# Review workflow
60349
-
60350
- How to work Dosu's review queue from the CLI: triage pending items, read the
60351
- diff, optionally edit, and apply a decision — safely.
60352
-
60353
- The queue holds **document versions** (AI-generated, user-edited, synced from
60354
- source, or API-created) and **draft messages**, all surfaced through the same
60355
- \`dosu review\` commands. Items are keyed on a single \`page-version-id\` so
60356
- \`list\` → \`diff\` → \`edit\` → \`approve\`/\`reject\` all share one ID.
60357
-
60358
- > \`list\` returns only the most recent pending items (server-capped, currently
60359
- > 50) and does not flag truncation. If the queue is large, don't assume \`list\`
60360
- > shows everything.
60361
-
60362
- ## Scope tool access for a review session
60363
-
60364
- Approving and rejecting are destructive and outward-facing. When you spin up an
60365
- agent session dedicated to reviewing, scope it to just the review commands:
60366
-
60367
- \`\`\`yaml
60368
- allowed-tools: Bash(dosu review:*)
60369
- \`\`\`
60370
-
60371
- This lets the agent triage, diff, and edit, but keeps it from running unrelated
60372
- CLI commands during a review pass.
60373
-
60374
- ## The flow
60375
-
60376
- \`\`\`bash
60377
- # 1. Triage — what's pending? Columns: ID, Kind, Title, Source, Status, Created.
60378
- dosu review list --json
60379
-
60380
- # 2. Inspect — read the exact change before deciding anything.
60381
- dosu review diff <page-version-id> --json
60382
-
60383
- # 3. (optional) Edit in place instead of rejecting a near-miss.
60384
- dosu review edit <page-version-id> --body-file ./revised.md # or --body / --title
60385
-
60386
- # 4. Decide — only on the item the user named, only after they OK it.
60387
- dosu review approve <page-version-id> --confirm --json
60388
- dosu review reject <page-version-id> --confirm --json
60389
-
60390
- # Undo a decision — back to pending.
60391
- dosu review revert <page-version-id> --json
60392
- \`\`\`
60393
-
60394
- ### \`--confirm\` is mandatory for agents
60395
-
60396
- \`approve\` and \`reject\` only prompt interactively in a TTY *without* \`--json\`.
60397
- The skill always passes \`--json\`, which suppresses both the diff preview and
60398
- the prompt — so without \`--confirm\` the command changes nothing and returns
60399
- \`{ "applied": false, "confirmRequired": true }\`. Run \`dosu review diff <id>\` to
60400
- see the change, then pass \`--confirm\` to apply — only after the user has
60401
- approved that specific item.
60402
-
60403
- ## Reading the \`Source\` column
60404
-
60405
- \`Source\` is the humanized \`origin\` field:
60406
-
60407
- | \`origin\` | Source | Meaning |
60408
- |---|---|---|
60409
- | \`manual_update\` | User created / User updated | A teammate authored or edited the doc |
60410
- | \`llm_generated\` | AI generated | Dosu drafted the change |
60411
- | \`sync_upstream\` | Synced from source | Came in from a connected source (Notion/Confluence/GitHub) |
60412
- | \`api_update\` | Created via API | Pushed in programmatically |
60413
-
60414
- ## Feeding PR / thread context
60415
-
60416
- A review often originates from a conversation thread (a GitHub issue, a Slack
60417
- message, a sync-back PR). Pull that context before deciding:
60418
-
60419
- \`\`\`bash
60420
- dosu review context <thread-id> --json
60421
- \`\`\`
60422
-
60423
- It returns the review \`type\`, the review/published page IDs, and — when the
60424
- change round-trips to an external system — a **Sync PR URL**. Use it to:
60425
-
60426
- - show the user where the change came from and where approval will land, and
60427
- - read the originating discussion so the decision matches intent.
60428
-
60429
- ## Safety rules
60430
-
60431
- - **Explicit item only.** Decide on the \`id\` the user named. "Review my queue"
60432
- → run \`list\` and show it; never decide on their behalf.
60433
- - **Diff before deciding.** Always \`diff\` and surface the change first.
60434
- - **Never batch-accept.** No loop over \`list\` that approves everything. One
60435
- item, one confirmation.
60436
- - **Extra care for sync/PR origins.** \`Synced from source\` items and anything
60437
- with a Sync PR URL push back to an upstream system on approval. Call it out
60438
- and get explicit sign-off.
60439
- `,
60549
+ content: '# Review workflow\n\nHow to work Dosu\'s review queue from the CLI: triage pending items, read the\ndiff, optionally edit, and apply a decision — safely.\n\nThe queue holds **document versions** (AI-generated, user-edited, synced from\nsource, or API-created) and **draft messages**, all surfaced through the same\n`dosu review` commands. Doc changes come from the selected Library; draft\nreplies come only from threads of the selected MCP deployment.\n\n## Read the list result\n\n`dosu review list --json` returns `{ "items": [...], "truncated": <bool>, "total": <n> }`.\n\n- `items` holds at most 50 entries, newest first, doc changes and draft\n replies merged.\n- `truncated: true` means more pending items may exist than `items` shows.\n- `total` counts what the server found before that cap. When `truncated` is\n true it is a lower bound: report "at least `total`", never an exact backlog.\n- Each `id` is opaque. A doc change\'s `id` is its page-version UUID; a draft\n reply\'s `id` looks like `draft_message:<uuid>` and has `kind: "draft_message"`.\n Pass the `id` exactly as listed to `diff`, `edit`, `approve`, and `reject`;\n do not strip the prefix or shorten it.\n- `--since <when>` and `--until <when>` narrow the list by creation time. Each\n takes a duration back from now (`24h`, `7d`, `2w`), a UTC date\n (`2026-09-01`; an `--until` date includes that whole day), or an ISO-8601\n datetime. When `truncated` is true, `--until` reaches older items.\n\nAn empty `items` array means no pending items in the current scope. It is not\na reason to switch deployments.\n\n## Check the context when the scope looks wrong\n\nRun this only when the user says the list is for the wrong Library, names a\ndifferent Library, or a command reports missing context:\n\n1. `dosu status --json`: login state and the selected MCP deployment.\n2. `dosu deployments info --json`: the selected MCP deployment. The list reads\n the Library saved in the CLI\'s config, which `dosu setup` and\n `dosu deployments switch` set to this deployment\'s `space_id`\n (`dosu libraries info <space_id> --json` gives its name).\n3. If that is not the Library the user means, run\n `dosu deployments list --json` and find the MCP deployment whose `space_id`\n matches it (`dosu libraries list --json` maps names to IDs).\n4. Switch only to a deployment the user named or confirmed:\n `dosu deployments switch <deployment-id> --json`. It changes the CLI\'s\n selection for later commands and mints a new API key, so say that it did.\n Never pick the first deployment, and ask when several match or none does.\n5. Run `dosu review list --json` again.\n\n## Draft reply limits\n\n- `edit` on a draft accepts `--body` or `--body-file` only (no `--title`).\n- `approve` publishes the draft to its thread; `reject` discards it.\n- Drafts have no `revert`. A rejected draft is regenerated on the next agent run.\n\n## Scope tool access for a review session\n\nApproving and rejecting are destructive and outward-facing. When you spin up an\nagent session dedicated to reviewing, scope it to just the review commands:\n\n```yaml\nallowed-tools: Bash(dosu review:*)\n```\n\nThis lets the agent triage, diff, and edit, but keeps it from running unrelated\nCLI commands during a review pass.\n\n## The flow\n\n```bash\n# 1. Triage — what\'s pending? Columns: ID, Kind, Title, Source, Status, Created.\ndosu review list --json\ndosu review list --since 7d --json # optional creation-time window\n\n# 2. Inspect — read the exact change before deciding anything.\ndosu review diff <id> --json\n\n# 3. (optional) Edit in place instead of rejecting a near-miss.\ndosu review edit <id> --body-file ./revised.md # or --body / --title (docs only)\n\n# 4. Decide — only on the item the user named, only after they OK it.\ndosu review approve <id> --confirm --json\ndosu review reject <id> --confirm --json\n\n# Undo a doc-change decision — back to pending (not for draft replies).\ndosu review revert <page-version-id> --json\n```\n\n### `--confirm` is mandatory for agents\n\n`approve` and `reject` only prompt interactively in a TTY *without* `--json`.\nThe skill always passes `--json`, which suppresses both the diff preview and\nthe prompt — so without `--confirm` the command changes nothing and returns\n`{ "applied": false, "confirmRequired": true }`. Run `dosu review diff <id>` to\nsee the change, then pass `--confirm` to apply — only after the user has\napproved that specific item.\n\n## Reading the `Source` column\n\n`Source` is the humanized `origin` field:\n\n| `origin` | Source | Meaning |\n|---|---|---|\n| `manual_update` | User created / User updated | A teammate authored or edited the doc |\n| `llm_generated` | AI generated | Dosu drafted the change |\n| `sync_upstream` | Synced from source | Came in from a connected source (Notion/Confluence/GitHub) |\n| `api_update` | Created via API | Pushed in programmatically |\n\n## Feeding PR / thread context\n\nA review often originates from a conversation thread (a GitHub issue, a Slack\nmessage, a sync-back PR). Pull that context before deciding:\n\n```bash\ndosu review context <thread-id> --json\n```\n\nIt returns the review `type`, the review/published page IDs, and — when the\nchange round-trips to an external system — a **Sync PR URL**. Use it to:\n\n- show the user where the change came from and where approval will land, and\n- read the originating discussion so the decision matches intent.\n\n## Safety rules\n\n- **Explicit item only.** Decide on the `id` the user named. "Review my queue"\n → run `list` and show it; never decide on their behalf.\n- **Listing is read-only.** A request to show the list authorizes no approve,\n reject, edit, API-key creation, or deployment switch.\n- **Diff before deciding.** Always `diff` and surface the change first.\n- **Never batch-accept.** No loop over `list` that approves everything. One\n item, one confirmation.\n- **Extra care for sync/PR origins.** `Synced from source` items and anything\n with a Sync PR URL push back to an upstream system on approval. Call it out\n and get explicit sign-off.\n',
60440
60550
  executable: false
60441
60551
  },
60442
60552
  {
@@ -60676,7 +60786,7 @@ import {
60676
60786
  symlinkSync,
60677
60787
  writeFileSync as writeFileSync12
60678
60788
  } from "node:fs";
60679
- import { homedir as homedir8 } from "node:os";
60789
+ import { homedir as homedir7 } from "node:os";
60680
60790
  import { dirname as dirname10, join as join22, relative as relative3 } from "node:path";
60681
60791
  function skillAgentIDsForProviders(providerIDs) {
60682
60792
  return [
@@ -60687,15 +60797,15 @@ function isSkillAgentID(agent) {
60687
60797
  return SUPPORTED_SKILL_AGENTS.includes(agent);
60688
60798
  }
60689
60799
  function universalSkillsDir() {
60690
- return join22(homedir8(), ".agents", "skills");
60800
+ return join22(homedir7(), ".agents", "skills");
60691
60801
  }
60692
60802
  function linkedSkillsDir(agent) {
60693
60803
  if (agent === "claude-code") {
60694
- const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim() || join22(homedir8(), ".claude");
60804
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim() || join22(homedir7(), ".claude");
60695
60805
  return join22(claudeConfigDir, "skills");
60696
60806
  }
60697
60807
  if (agent === "windsurf")
60698
- return join22(homedir8(), ".codeium", "windsurf", "skills");
60808
+ return join22(homedir7(), ".codeium", "windsurf", "skills");
60699
60809
  return null;
60700
60810
  }
60701
60811
  function skillInstallTargetForProvider(providerID) {
@@ -60738,7 +60848,7 @@ function clearSkillInstallState() {
60738
60848
  rmSync5(installStatePath(), { force: true });
60739
60849
  }
60740
60850
  function pruneLegacySkillLock() {
60741
- const lockPath = join22(homedir8(), ".agents", ".skill-lock.json");
60851
+ const lockPath = join22(homedir7(), ".agents", ".skill-lock.json");
60742
60852
  try {
60743
60853
  if (!existsSync19(lockPath))
60744
60854
  return [];
@@ -62173,15 +62283,15 @@ var init_vscode = __esm(() => {
62173
62283
  });
62174
62284
 
62175
62285
  // src/mcp/providers/windsurf.ts
62176
- import { homedir as homedir9 } from "node:os";
62286
+ import { homedir as homedir8 } from "node:os";
62177
62287
  import { join as join36 } from "node:path";
62178
62288
  var WindsurfProvider = () => createJSONProvider({
62179
62289
  providerName: "Windsurf",
62180
62290
  providerID: "windsurf",
62181
62291
  local: false,
62182
62292
  priorityValue: 9,
62183
- paths: [join36(homedir9(), ".codeium", "windsurf")],
62184
- globalPath: join36(homedir9(), ".codeium", "windsurf", "mcp_config.json"),
62293
+ paths: [join36(homedir8(), ".codeium", "windsurf")],
62294
+ globalPath: join36(homedir8(), ".codeium", "windsurf", "mcp_config.json"),
62185
62295
  topKey: "mcpServers"
62186
62296
  });
62187
62297
  var init_windsurf = __esm(() => {
@@ -62190,14 +62300,14 @@ var init_windsurf = __esm(() => {
62190
62300
 
62191
62301
  // src/mcp/providers/zed.ts
62192
62302
  import { rmSync as rmSync6 } from "node:fs";
62193
- import { homedir as homedir10, platform as platform3 } from "node:os";
62303
+ import { homedir as homedir9, platform as platform3 } from "node:os";
62194
62304
  import { join as join37 } from "node:path";
62195
62305
  function zedConfigDir() {
62196
62306
  const os = platform3();
62197
62307
  if (os === "win32")
62198
62308
  return join37(appSupportDir(), "Zed");
62199
62309
  if (os === "darwin")
62200
- return join37(homedir10(), ".config", "zed");
62310
+ return join37(homedir9(), ".config", "zed");
62201
62311
  return join37(appSupportDir(), "zed");
62202
62312
  }
62203
62313
  function zedDataDir() {
@@ -62206,7 +62316,7 @@ function zedDataDir() {
62206
62316
  return join37(appSupportDir(), "Zed");
62207
62317
  if (os === "win32")
62208
62318
  return join37(process.env.LOCALAPPDATA ?? "", "Zed");
62209
- return join37(process.env.XDG_DATA_HOME ?? join37(homedir10(), ".local", "share"), "zed");
62319
+ return join37(process.env.XDG_DATA_HOME ?? join37(homedir9(), ".local", "share"), "zed");
62210
62320
  }
62211
62321
  function legacyDarwinSettingsPath() {
62212
62322
  return platform3() === "darwin" ? join37(appSupportDir(), "Zed", "settings.json") : null;
@@ -62355,7 +62465,7 @@ import {
62355
62465
  writeFileSync as writeFileSync14,
62356
62466
  writeSync
62357
62467
  } from "node:fs";
62358
- import { homedir as homedir11 } from "node:os";
62468
+ import { homedir as homedir10 } from "node:os";
62359
62469
  import { join as join38 } from "node:path";
62360
62470
  function statePath() {
62361
62471
  return join38(getConfigDir(), STATE_FILENAME2);
@@ -62490,7 +62600,7 @@ function startAutoUpdate(version, options = {}) {
62490
62600
  return claim === "held" ? "in_progress" : "unavailable";
62491
62601
  try {
62492
62602
  const child = spawn3(invocation.command, invocation.args, {
62493
- cwd: homedir11(),
62603
+ cwd: homedir10(),
62494
62604
  detached: true,
62495
62605
  stdio: "ignore",
62496
62606
  windowsHide: true,
@@ -62891,7 +63001,7 @@ var init_follow = () => {};
62891
63001
 
62892
63002
  // src/sessions/session-title.ts
62893
63003
  import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync25, renameSync as renameSync6, statSync as statSync10, writeFileSync as writeFileSync17 } from "node:fs";
62894
- import { homedir as homedir12 } from "node:os";
63004
+ import { homedir as homedir11 } from "node:os";
62895
63005
  import { join as join41 } from "node:path";
62896
63006
  function cachePath2(configDir) {
62897
63007
  return join41(configDir, CACHE_FILENAME5);
@@ -62946,7 +63056,7 @@ function claudeSummaryFromHead(text) {
62946
63056
  }
62947
63057
  return null;
62948
63058
  }
62949
- function reconstructSession(harness, id, slug, home = homedir12(), exists = existsSync26) {
63059
+ function reconstructSession(harness, id, slug, home = homedir11(), exists = existsSync26) {
62950
63060
  if (!slug)
62951
63061
  return null;
62952
63062
  const candidates = [];
@@ -63622,13 +63732,13 @@ var init_activity_view = __esm(() => {
63622
63732
 
63623
63733
  // src/rules/installer.ts
63624
63734
  import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync27, unlinkSync as unlinkSync5, writeFileSync as writeFileSync18 } from "node:fs";
63625
- import { homedir as homedir13 } from "node:os";
63735
+ import { homedir as homedir12 } from "node:os";
63626
63736
  import { dirname as dirname12, join as join42 } from "node:path";
63627
63737
  function claudeConfigDir4() {
63628
- return process.env.CLAUDE_CONFIG_DIR || join42(homedir13(), ".claude");
63738
+ return process.env.CLAUDE_CONFIG_DIR || join42(homedir12(), ".claude");
63629
63739
  }
63630
63740
  function codexHome4() {
63631
- return process.env.CODEX_HOME || join42(homedir13(), ".codex");
63741
+ return process.env.CODEX_HOME || join42(homedir12(), ".codex");
63632
63742
  }
63633
63743
  function isRuleAgent(agent) {
63634
63744
  return Object.hasOwn(RULE_TARGETS, agent);
@@ -63774,7 +63884,7 @@ var init_installer = __esm(() => {
63774
63884
  },
63775
63885
  cursor: {
63776
63886
  kind: "file",
63777
- path: () => join42(homedir13(), ".cursor", "rules", "dosu.mdc"),
63887
+ path: () => join42(homedir12(), ".cursor", "rules", "dosu.mdc"),
63778
63888
  cursorFrontmatter: true
63779
63889
  },
63780
63890
  codex: {
@@ -63783,15 +63893,15 @@ var init_installer = __esm(() => {
63783
63893
  },
63784
63894
  opencode: {
63785
63895
  kind: "section",
63786
- path: () => join42(homedir13(), ".config", "opencode", "AGENTS.md")
63896
+ path: () => join42(homedir12(), ".config", "opencode", "AGENTS.md")
63787
63897
  },
63788
63898
  gemini: {
63789
63899
  kind: "section",
63790
- path: () => join42(homedir13(), ".gemini", "GEMINI.md")
63900
+ path: () => join42(homedir12(), ".gemini", "GEMINI.md")
63791
63901
  },
63792
63902
  antigravity: {
63793
63903
  kind: "section",
63794
- path: () => join42(homedir13(), ".gemini", "GEMINI.md")
63904
+ path: () => join42(homedir12(), ".gemini", "GEMINI.md")
63795
63905
  }
63796
63906
  };
63797
63907
  });
@@ -65582,7 +65692,7 @@ var init_flow2 = __esm(() => {
65582
65692
  // src/commands/upgrade.ts
65583
65693
  import { spawnSync } from "node:child_process";
65584
65694
  import { realpathSync as realpathSync2 } from "node:fs";
65585
- import { homedir as homedir14 } from "node:os";
65695
+ import { homedir as homedir13 } from "node:os";
65586
65696
  import { posix, win32 as win322 } from "node:path";
65587
65697
  function isAbsoluteCommandProcessor(value) {
65588
65698
  return Boolean(value && win322.isAbsolute(value) && win322.basename(value).toLowerCase() === "cmd.exe");
@@ -65636,7 +65746,7 @@ function packageRoots(manager, platform, comSpec, env, cwd) {
65636
65746
  manager === "npm" ? path.join(lines[0], "@dosu", "cli") : path.join(lines[0], "node_modules", "@dosu", "cli")
65637
65747
  ];
65638
65748
  }
65639
- function owningPackageManager(entrypoint, platform, comSpec, env = process.env, cwd = homedir14()) {
65749
+ function owningPackageManager(entrypoint, platform, comSpec, env = process.env, cwd = homedir13()) {
65640
65750
  const path = platform === "win32" ? win322 : posix;
65641
65751
  if (!entrypoint || !path.isAbsolute(entrypoint))
65642
65752
  return null;
@@ -65689,7 +65799,7 @@ Or install Dosu globally with your package manager:`);
65689
65799
  function runUpgrade(channel = INSTALL_CHANNEL, options = {}) {
65690
65800
  const platform = options.platform ?? process.platform;
65691
65801
  const env = options.env ?? process.env;
65692
- const cwd = options.cwd ?? homedir14();
65802
+ const cwd = options.cwd ?? homedir13();
65693
65803
  let manager = null;
65694
65804
  if (channel === "npm") {
65695
65805
  if (isNpxInvocation(channel, env)) {
@@ -65756,7 +65866,7 @@ function finishWithNewBinary(channel, options) {
65756
65866
  return;
65757
65867
  }
65758
65868
  const result = spawnSync(invocation.command, invocation.args, {
65759
- cwd: options.cwd ?? homedir14(),
65869
+ cwd: options.cwd ?? homedir13(),
65760
65870
  shell: false,
65761
65871
  stdio: "inherit"
65762
65872
  });
@@ -66554,7 +66664,6 @@ var init_pages_view = __esm(() => {
66554
66664
  });
66555
66665
 
66556
66666
  // src/tui/tui.ts
66557
- import { homedir as homedir15 } from "node:os";
66558
66667
  import { basename as basename6 } from "node:path";
66559
66668
  async function probeSession(cfg) {
66560
66669
  if (!isAuthenticated(cfg) || !isTokenExpired(cfg))
@@ -66783,8 +66892,9 @@ async function runSettings(cfg) {
66783
66892
  while (true) {
66784
66893
  const target = cfg.active_account?.target;
66785
66894
  const library = target?.library_name ?? target?.deployment_name ?? "not configured";
66786
- const filter = loadSyncState().project_filter;
66787
- const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename6(dir)).join(", ") : `${filter.length} projects`;
66895
+ const sync = loadSyncState();
66896
+ const filter = sync.repo_filter;
66897
+ const scope = !filter ? sync.project_filter?.length ? `${sync.project_filter.length} folders (legacy)` : "all repos" : filter.length === 0 ? "no repos" : filter.length <= 2 ? filter.map(displayRepo).join(", ") : `${filter.length} repos`;
66788
66898
  const action = await menuSelect("settings", [
66789
66899
  { label: "switch organization", hint: target?.org_name, value: "switch-org" },
66790
66900
  { label: "switch library", hint: library, value: "switch-library" },
@@ -66818,45 +66928,43 @@ async function runSettings(cfg) {
66818
66928
  }
66819
66929
  }
66820
66930
  }
66821
- function displayDir(dir) {
66822
- const home = homedir15();
66823
- return dir === home ? "~" : dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir;
66824
- }
66825
- function discoverProjectDirs() {
66931
+ function discoverSessionRepos(current) {
66826
66932
  const counts = new Map;
66827
66933
  try {
66828
66934
  const resolver = createProjectDirResolver();
66829
66935
  for (const session of scanAgentSessions({})) {
66830
- const key = resolver.resolve(session) ?? UNKNOWN_PROJECT;
66831
- counts.set(key, (counts.get(key) ?? 0) + 1);
66936
+ const repo = resolver.resolveRepo(session);
66937
+ if (repo)
66938
+ counts.set(repo, (counts.get(repo) ?? 0) + 1);
66832
66939
  }
66833
66940
  resolver.flush();
66834
66941
  } catch {}
66835
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([dir]) => dir);
66942
+ for (const repo of current ?? [])
66943
+ if (!counts.has(repo))
66944
+ counts.set(repo, 0);
66945
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
66836
66946
  }
66837
66947
  async function runStudyingProjectsSetting() {
66838
- const dirs = discoverProjectDirs();
66839
- if (dirs.length === 0) {
66840
- log2.info("No local agent sessions found yet; nothing to scope.");
66948
+ const state = loadSyncState();
66949
+ const current = studyRepoFilter(state, () => scanAgentSessions({}), createProjectDirResolver());
66950
+ const repos = discoverSessionRepos(current);
66951
+ if (repos.length === 0) {
66952
+ log2.info("No agent sessions inside a git repo found yet; nothing to scope.");
66841
66953
  return;
66842
66954
  }
66843
- const current = loadSyncState().project_filter;
66844
66955
  const selected = await multiselect2({
66845
- message: "Study sessions from which folders?",
66846
- options: dirs.map((dir) => ({
66847
- label: dir === UNKNOWN_PROJECT ? "(unknown folder)" : displayDir(dir),
66848
- value: dir
66849
- })),
66850
- initialValues: current?.length ? current : dirs,
66851
- summary: (picked) => picked.length === dirs.length ? "all folders · new ones included automatically" : `${picked.length} of ${dirs.length} folders · subfolders included`,
66852
- validate: (picked) => picked.length === 0 ? "Select at least one folder." : undefined
66956
+ message: "Study sessions from which repos?",
66957
+ options: repos.map((repo) => ({ label: displayRepo(repo), hint: repo, value: repo })),
66958
+ initialValues: current ?? repos,
66959
+ summary: (picked) => picked.length === repos.length ? "all repos · new ones included automatically" : `${picked.length} of ${repos.length} repos`,
66960
+ validate: (picked) => picked.length === 0 ? "Select at least one repo." : undefined
66853
66961
  });
66854
66962
  if (isCancel2(selected))
66855
66963
  return;
66856
- const { project_filter: _previous, ...state } = loadSyncState();
66857
- const all = selected.length === dirs.length;
66858
- saveSyncState(all ? state : { ...state, project_filter: [...selected] });
66859
- const scope = all ? "all folders" : selected.map(displayDir).join(", ");
66964
+ const { repo_filter: _repos, project_filter: _folders, ...fresh } = loadSyncState();
66965
+ const all = selected.length === repos.length;
66966
+ saveSyncState(all ? fresh : { ...fresh, repo_filter: [...selected] });
66967
+ const scope = all ? "all repos" : selected.map(displayRepo).join(", ");
66860
66968
  log2.success(`Study scope ${dim(`· ${scope}`)}`);
66861
66969
  }
66862
66970
  async function handleAuthenticate(cfg) {
@@ -66909,6 +67017,7 @@ var init_tui = __esm(() => {
66909
67017
  init_providers();
66910
67018
  init_generate();
66911
67019
  init_project_dir();
67020
+ init_repo();
66912
67021
  init_scan();
66913
67022
  init_agents_md_step();
66914
67023
  init_flow2();
@@ -67631,7 +67740,7 @@ function createProgram(options = {}) {
67631
67740
  const program = new Command;
67632
67741
  let telemetryStarted = false;
67633
67742
  let telemetryValidationFailure = false;
67634
- program.name("dosu").description("Dosu CLI - Manage MCP servers for AI tools").version(getVersionString(), "-v, --version").helpCommand("help [command]", "Show help for a command").option("--debug", "Enable debug logging to stderr", false).hook("preAction", async (thisCommand, actionCommand) => {
67743
+ program.name("dosu").description("Dosu CLI - Set up Dosu MCP for AI tools and work with Dosu Libraries, docs, reviews, and more").version(getVersionString(), "-v, --version").helpCommand("help [command]", "Show help for a command").option("--debug", "Enable debug logging to stderr", false).hook("preAction", async (thisCommand, actionCommand) => {
67635
67744
  const opts = thisCommand.optsWithGlobals();
67636
67745
  logger.init({ debug: opts.debug });
67637
67746
  if (shouldRunBackgroundChecks(actionCommand.name())) {
@@ -68117,4 +68226,4 @@ main().catch((err) => {
68117
68226
  process.exit(1);
68118
68227
  });
68119
68228
 
68120
- //# debugId=da3b44a3-063b-fb2c-6475-6e2164756e21
68229
+ //# debugId=01f74cf6-cdbb-b296-6475-6e2164756e21
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "type": "module",
5
- "description": "Dosu CLI - Manage MCP servers for AI tools",
5
+ "description": "Dosu CLI - Set up Dosu MCP for AI tools and work with Dosu Libraries, docs, and reviews",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",