@dosu/cli 0.67.0-beta.1 โ†’ 0.67.0-beta.3

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 +35 -63
  2. package/bin/dosu.js +228 -151
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -99,18 +99,15 @@ Or right-click the binary, select "Open", and click "Open" in the dialog.
99
99
  | `dosu upgrade` | Update Dosu through the package manager that installed it, re-running `dosu setup` when the update changes agent config |
100
100
  | `dosu mcp list` | List supported AI tools |
101
101
  | `dosu mcp add <tool>` | Add the Dosu MCP server to a specific tool |
102
- | `dosu mcp refresh` | Rewrite the Dosu MCP entry in every already-configured tool from the current setup, and re-apply the hooks of every agent that has them on |
103
- | `dosu mcp serve --client <agent>` | The local (stdio) Dosu MCP server that tools' MCP entries run; not run by hand |
104
- | `dosu project show\|link\|unlink [dir]` | Show the project key Dosu memory scopes a directory by, or link a directory to a key |
105
- | `dosu memory search <query>` / `dosu memory evidence <id>` | Search Dosu memory, or read the evidence behind one memory, as an agent's MCP tools do |
102
+ | `dosu mcp refresh` | Rewrite the Dosu MCP entry in every already-configured tool from the current setup |
106
103
  | `dosu logs` | View or manage debug logs (`--tail`, `--clear`) |
107
104
  | `dosu telemetry` | Manage usage analytics and error diagnostics (`status`, `enable`, `disable`, `reset`) |
108
105
 
109
- `dosu mcp add` takes `-g, --global` to install for all projects instead of project-local, and `--show-secret` to print the full manual config. The entry it writes runs `dosu mcp serve`, a local proxy to Dosu's MCP endpoint that adds the API key from the CLI config and the project, branch, and agent of the session, so memory lookups are scoped to the codebase you are in. The entry runs the Dosu install that wrote it, by absolute path (the npm package through the `node` that ran it), so an older `dosu` elsewhere on PATH or a PATH without `node` does not matter. A one-off `npx @dosu/cli setup` (or `bunx`, `pnpm dlx`) writes a remote entry instead, since the runner's copy does not last.
106
+ `dosu mcp add` takes `-g, --global` to install for all projects instead of project-local, and `--show-secret` to print the full manual config.
110
107
 
111
- `dosu upgrade` delegates to npm, pnpm, Yarn Classic, or Homebrew only after confirming which manager owns the current installation. Temporary package-runner invocations stay ephemeral, ambiguous or local installs are left unchanged, and standalone binaries receive the latest safe manual download path. After a successful update it hands off to the new version, which re-applies bundled skills and enabled agent hooks. If configured MCP entries differ from the shape the new version writes, it runs `dosu setup`, or refreshes only those entries without a TTY. Upgrades done outside `dosu upgrade` get the same safety net on the first command after each version change, including between betas: stale MCP entries are rewritten and enabled hooks and skills are re-applied. Disabled hooks stay disabled.
108
+ `dosu upgrade` delegates to npm, pnpm, Yarn Classic, or Homebrew only after confirming which manager owns the current installation. Temporary package-runner invocations stay ephemeral, ambiguous or local installs are left unchanged, and standalone binaries receive the latest safe manual download path. After a successful update it hands off to the new version, which re-applies the bundled skills and, only when a configured agent's MCP entry is out of date, runs `dosu setup` (or the non-interactive `dosu mcp refresh` without a TTY). An entry is out of date when it doesn't match the shape the current provider code writes. Upgrades done outside `dosu upgrade` (npm, brew, `npx @dosu/cli@latest`) get a safety net: on the first command of a new version, the CLI checks each configured provider, silently rewrites only the out-of-date entries, and prompts you to run `dosu setup` for the rest; the bundled agent skills are always re-applied on the first command after any version change.
112
109
 
113
- Dosu also updates itself automatically. When the update check (every six hours) finds a newer release, npm, pnpm, Yarn Classic, and Homebrew installs start the same package-manager update in a detached background process. The current command runs unchanged, and your next command uses the new version, whose first run applies the skill, MCP, and hooks refreshes above. Rules and the status line still need `dosu setup` when a release changes them. Background updates never run for `npx` invocations, standalone binaries, CI, or `DOSU_DEV=true`. A failed install (for example, a global npm prefix that needs `sudo`) falls back to the "Run `dosu upgrade`" notice and is retried after six hours. Turn background updates off with `dosu upgrade --auto off` (and back on with `--auto on`), or with `DOSU_DISABLE_AUTOUPDATE=1` for a single environment.
110
+ Dosu also updates itself automatically. When the update check (every six hours) finds a newer release, npm, pnpm, Yarn Classic, and Homebrew installs start the same package-manager update in a detached background process. The current command runs unchanged, and your next command uses the new version, whose first run applies the skill and MCP refreshes above. Hooks, rules, and the status line still need `dosu setup` when a release changes them. Background updates never run for `npx` invocations, standalone binaries, CI, or `DOSU_DEV=true`. A failed install (for example, a global npm prefix that needs `sudo`) falls back to the "Run `dosu upgrade`" notice and is retried after six hours. Turn background updates off with `dosu upgrade --auto off` (and back on with `--auto on`), or with `DOSU_DISABLE_AUTOUPDATE=1` for a single environment.
114
111
 
115
112
  ### Platform commands
116
113
 
@@ -158,7 +155,6 @@ To change a skill, edit `skills/<name>/โ€ฆ` and run `bun run embed:skills` to re
158
155
  | `cline-cli` | Cline CLI |
159
156
  | `copilot` | GitHub Copilot CLI |
160
157
  | `opencode` | OpenCode |
161
- | `pi` | Pi (installs the Dosu pi extension; global only) |
162
158
  | `antigravity` | Antigravity |
163
159
  | `mcporter` | MCPorter |
164
160
  | `factory` | Factory |
@@ -174,63 +170,36 @@ dosu setup --agent --tool claude
174
170
 
175
171
  Combine with `dosu login --request` / `--check <ticket>` for human-in-the-loop authentication, and `--mode oss|cloud` to skip the mode prompt.
176
172
 
177
- ### Syncing sessions to Dosu memory: status line and incognito
178
-
179
- With `dosu knowledge hooks enable`, finished coding-agent sessions ship to Dosu memory in the
180
- background (secrets redacted locally first), and Dosu learns from them server-side. A Claude Code
181
- or Codex (0.160+) session ships as soon as it ends, an OpenCode session as soon as the opencode
182
- process that ran it exits, however it exits, and a pi session as soon as pi shuts it down. OpenCode
183
- gets a plugin and pi an extension rather than a hook; both also add memory digests to prompts, and
184
- pi's registers Dosu's MCP server (the `dosu mcp serve` proxy) with pi's built-in MCP and adds
185
- `/dosu-incognito` (pi run with `--no-extensions` skips it, see
186
- [docs/syncing.md](docs/syncing.md#pi)). Cursor sessions ship with a
187
- later sync once they have been quiet for five minutes (its end event fires every turn). A resumed
188
- session later ships only what is new. Shipping is on by default; `dosu knowledge transcripts
189
- disable` turns it off.
190
- `dosu knowledge sync --status` and `dosu knowledge sessions` show what shipped and why anything did
191
- not (too short, rejected by the server, or from an agent not supported yet);
192
- `dosu knowledge sync --retry-rejected` tries refused sessions again. On a throwaway machine (a VM
193
- destroyed after its last task), run `dosu knowledge sync --flush` as the last step before teardown:
194
- it ships every session not shipped yet right away, including the ones no end event named (Cursor's,
195
- a Codex session killed with SIGTERM, pi run with `--no-extensions`); see
196
- [docs/syncing.md](docs/syncing.md#throwaway-machines). Running `claude -p` from an eval
197
- harness? Some flags (`--bare`, `--safe-mode`, `--no-session-persistence`, ...) keep sessions from
198
- reaching Dosu; see [docs/syncing.md](docs/syncing.md#claude-code-in-an-eval-harness).
199
-
200
- When the deployment has no linked repository, memory is scoped by the codebase a session worked in:
201
- its `origin` remote, else the repository's root commit, else its path. Set `DOSU_PROJECT` in the
202
- agent's environment, or run `dosu project link [dir] <key>`, to name it yourself
203
- (`dosu project show` prints the key in use). A session keeps the key it was first resolved under. See
204
- [docs/syncing.md](docs/syncing.md#project-key).
205
-
206
- Two switches make shipping visible and controllable. `dosu setup` installs the status line and each
207
- agent's incognito command alongside the hook (`dosu knowledge hooks enable` installs the incognito
208
- command with each agent's hooks); they can also be managed directly:
173
+ ### Studying sessions: status line and incognito
174
+
175
+ With `dosu knowledge hooks enable`, Dosu studies finished coding-agent sessions in the background
176
+ and turns what it learns into shared knowledge. Two switches make that visible and controllable per
177
+ session. `dosu setup` installs both alongside the hook; they can also be managed directly:
209
178
 
210
179
  ```bash
211
- dosu knowledge statusline enable|disable [claude|cursor] # status-bar line in Claude Code / Cursor CLI
212
- dosu knowledge incognito on|off [claude|cursor|codex|opencode|pi] # turn Dosu off/on for an agent
213
- dosu knowledge incognito status # which agents are incognito
180
+ dosu knowledge statusline enable|disable [claude|cursor] # status-bar line in Claude Code / Cursor CLI
181
+ dosu knowledge incognito on|off [claude|cursor|codex] # stop/resume studying an agent's sessions
182
+ dosu knowledge incognito status # which agents are incognito
214
183
  ```
215
184
 
216
- The status line shows one of `๐Ÿ“š Dosu shippingโ€ฆ` (a sync run is live), `๐Ÿ“š Dosu on`,
217
- `๐Ÿ‘ป Dosu incognito`, `โšช Dosu paused`, `โšช Dosu not learning from this repo`, or `โšช Dosu off`.
218
- Every session is shipped unless you limit syncing to picked repos under `dosu` โ†’ settings โ†’ sync
219
- scope (or `dosu knowledge scope set <checkout>...`; `dosu knowledge skip-backlog` passes over the
220
- sessions waiting to ship, see [docs/syncing.md](docs/syncing.md#repo-scope)). Neither setup nor
221
- `enable` replaces a status line you already have; they print the one-liner to add to your own
222
- script instead.
223
-
224
- `incognito on` is a saved per-agent setting: none of that agent's sessions ship, its prompts get no
225
- memory lookup, and its Dosu tool calls (and `dosu memory` run as it) are refused without leaving the
226
- machine. Turning it off later does not ship the sessions that ran while it was on, even if you
227
- resume them, nor their subagents or forks (`off` seals any no sync has settled yet; see
228
- [docs/syncing.md](docs/syncing.md#per-agent-incognito)). To keep a single session out instead, run
229
- `/dosu-incognito` in it (in Codex, which has no user slash commands, mention the skill
230
- `$dosu-incognito`); setup installs it with the hook. It marks that session's transcript so it is
231
- never shipped (the whole session, and for the rest of it โ€” start a new session to turn Dosu back
232
- on) and tells the model not to use Dosu tools. See
233
- [docs/syncing.md](docs/syncing.md#per-session-incognito).
185
+ The status line shows one of `๐Ÿ“š Dosu studyingโ€ฆ` (a sync run is live), `๐Ÿ“š Dosu on`,
186
+ `๐Ÿ‘ป Dosu incognito`, `โšช Dosu paused`, `โšช Dosu not studying this repo`, or `โšช Dosu off`. Every
187
+ session is studied unless you limit studying to picked repos under `dosu` โ†’ settings โ†’ study
188
+ scope. In an interactive terminal with more than one repo, `dosu setup` asks which repos' sessions
189
+ to ship before offering the backfill of recent sessions. Selecting every repo clears the scope, so
190
+ new repos are included automatically. The choice is saved as the study scope; change it later under
191
+ `dosu` โ†’ settings โ†’ study scope or with `dosu knowledge scope set`. Notes from a session with a
192
+ known repo and branch are anchored to that branch; the rest are unanchored and reach topics
193
+ immediately. Neither setup nor `enable` replaces a status line you
194
+ already have; they print the one-liner to add to your own script instead.
195
+
196
+ `incognito on` is a saved per-agent setting: none of that agent's sessions are studied until you
197
+ turn it off. Turning it off resumes studying only new sessions going forward; sessions that
198
+ finished while incognito was on stay unstudied permanently, even if a later sync picks them up.
199
+ To keep a single chat out instead, type `/dosu-incognito` in it; setup installs that command with
200
+ the hook. It marks the session's transcript so studying skips it (the whole session, and for the
201
+ rest of it โ€” start a new session to turn Dosu back on) and tells the model not to use Dosu
202
+ tools. See [docs/studying.md](docs/studying.md).
234
203
 
235
204
  ### Telemetry and privacy
236
205
 
@@ -252,8 +221,11 @@ and may include documented coarse setup choices. `DO_NOT_TRACK=1` and
252
221
  `DOSU_TELEMETRY_DISABLED=1` disable all telemetry for the process. Dosu never collects
253
222
  prompts, raw command lines, free-form argument or option values, user source code, file contents,
254
223
  local paths, credentials, raw error messages, or `debug.log`. Environment-variable names and values
255
- are not collected. See [Telemetry and privacy](docs/telemetry.md) for the exact event fields,
256
- destinations, retention, and controls.
224
+ are not collected, with one exception: when a managed Claude Code settings conflict prevents a
225
+ study run, Dosu reports the allowlisted public Claude Code setting/variable names that caused the
226
+ conflict (never their values) to help diagnose why knowledge sync fails due to settings conflicts.
227
+ See [Telemetry and privacy](docs/telemetry.md) for the exact event fields, destinations, retention,
228
+ and controls.
257
229
 
258
230
  ## Configuration
259
231
 
package/bin/dosu.js CHANGED
@@ -450,7 +450,7 @@ function isNpxInvocation(channel = INSTALL_CHANNEL, env = process.env) {
450
450
  function getVersionString() {
451
451
  return `v${VERSION}`;
452
452
  }
453
- var VERSION = "0.67.0-beta.1", INSTALL_CHANNEL = "npm";
453
+ var VERSION = "0.67.0-beta.3", INSTALL_CHANNEL = "npm";
454
454
 
455
455
  // src/debug/logger.ts
456
456
  import {
@@ -7688,6 +7688,10 @@ function saveSyncState(state, configDir = getConfigDir()) {
7688
7688
  writeFileSync3(tmp, JSON.stringify(state, null, 2), { mode: 384 });
7689
7689
  renameSync2(tmp, path);
7690
7690
  }
7691
+ function saveRepoFilter(repos, configDir = getConfigDir()) {
7692
+ const { repo_filter: _repos, project_filter: _folders, ...rest } = loadSyncState(configDir);
7693
+ saveSyncState(repos ? { ...rest, repo_filter: [...repos] } : rest, configDir);
7694
+ }
7691
7695
  function setSyncPaused(paused, configDir = getConfigDir()) {
7692
7696
  const state = loadSyncState(configDir);
7693
7697
  if (paused)
@@ -9589,7 +9593,7 @@ var init_repo = __esm(() => {
9589
9593
  });
9590
9594
 
9591
9595
  // src/sync/lock.ts
9592
- import { existsSync as existsSync12, readFileSync as readFileSync15, rmSync as rmSync2, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
9596
+ import { existsSync as existsSync12, readFileSync as readFileSync15, rmSync as rmSync2, statSync as statSync5, utimesSync, writeFileSync as writeFileSync6 } from "node:fs";
9593
9597
  import { join as join15 } from "node:path";
9594
9598
  function processAlive(pid) {
9595
9599
  try {
@@ -9652,6 +9656,16 @@ function fileLock(configDir = getConfigDir(), now = () => new Date) {
9652
9656
  } catch {}
9653
9657
  return false;
9654
9658
  },
9659
+ refresh() {
9660
+ if (!held)
9661
+ return;
9662
+ try {
9663
+ if (readFileSync15(path, "utf8") === String(process.pid)) {
9664
+ const at = now();
9665
+ utimesSync(path, at, at);
9666
+ }
9667
+ } catch {}
9668
+ },
9655
9669
  release() {
9656
9670
  if (!held)
9657
9671
  return;
@@ -16186,76 +16200,6 @@ var init_ask = __esm(() => {
16186
16200
  import_picocolors7 = __toESM(require_picocolors(), 1);
16187
16201
  });
16188
16202
 
16189
- // src/setup/styles.ts
16190
- import { stripVTControlCharacters as stripVTControlCharacters4 } from "node:util";
16191
- function hasTruecolor() {
16192
- return /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
16193
- }
16194
- function brand(msg) {
16195
- if (!import_picocolors8.default.isColorSupported)
16196
- return msg;
16197
- return hasTruecolor() ? `${BRAND_FG}${msg}${RESET_FG}` : import_picocolors8.default.green(msg);
16198
- }
16199
- function brandBadge(msg) {
16200
- if (!import_picocolors8.default.isColorSupported)
16201
- return `[ ${msg} ]`;
16202
- const label = import_picocolors8.default.bold(` ${msg} `);
16203
- return hasTruecolor() ? `${BRAND_BG}${INK_FG}${label}${RESET_FG}${RESET_BG}` : import_picocolors8.default.bgGreen(import_picocolors8.default.black(label));
16204
- }
16205
- function dim(msg) {
16206
- return import_picocolors8.default.dim(msg);
16207
- }
16208
- function formatSetupSummary(title, items, marker = IconAdd) {
16209
- const styled = marker === IconRemove ? import_picocolors8.default.red(marker) : brand(marker);
16210
- const lines = items.map((item) => {
16211
- const path = `${item.path}${item.status ? ` (${item.status})` : ""}`;
16212
- return item.label ? `${styled} ${item.label}
16213
- ${dim(path)}` : `${styled} ${dim(path)}`;
16214
- });
16215
- return `${title}
16216
- ${lines.join(`
16217
- `)}`;
16218
- }
16219
- function browserFallbackHint(url) {
16220
- return dim(`If your browser doesn't open automatically, visit:
16221
- ${url}`);
16222
- }
16223
- function info(msg) {
16224
- return import_picocolors8.default.cyan(msg);
16225
- }
16226
- function visibleWidth(text) {
16227
- return stripVTControlCharacters4(text).length;
16228
- }
16229
- function wrapLog(text, columns = process.stdout.columns || 80) {
16230
- const width = Math.max(20, columns - LOG_GUTTER);
16231
- const out = [];
16232
- for (const paragraph of text.split(`
16233
- `)) {
16234
- if (visibleWidth(paragraph) <= width) {
16235
- out.push(paragraph);
16236
- continue;
16237
- }
16238
- let line = "";
16239
- for (const word of paragraph.split(" ")) {
16240
- if (line === "") {
16241
- line = word;
16242
- } else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
16243
- line = `${line} ${word}`;
16244
- } else {
16245
- out.push(line);
16246
- line = word;
16247
- }
16248
- }
16249
- out.push(line);
16250
- }
16251
- return out.join(`
16252
- `);
16253
- }
16254
- var import_picocolors8, IconAdd = "โœ”", IconRemove = "-", BRAND_FG = "\x1B[38;2;82;164;15m", BRAND_BG = "\x1B[48;2;82;164;15m", INK_FG = "\x1B[38;2;14;14;14m", RESET_FG = "\x1B[39m", RESET_BG = "\x1B[49m", LOG_GUTTER = 4;
16255
- var init_styles = __esm(() => {
16256
- import_picocolors8 = __toESM(require_picocolors(), 1);
16257
- });
16258
-
16259
16203
  // src/tui/layout.ts
16260
16204
  function contentWidth(columns = process.stdout.columns ?? 80) {
16261
16205
  return Math.min(columns, CONTENT_WIDTH);
@@ -16275,9 +16219,9 @@ function breadcrumb(segments, width = contentWidth()) {
16275
16219
  const trailWidth = head.reduce((total, segment) => total + segment.length + 3, 0);
16276
16220
  const room = Math.max(8, width - trailWidth);
16277
16221
  const clipped = leaf.length <= room ? leaf : `${leaf.slice(0, room - 1)}โ€ฆ`;
16278
- return head.map((segment) => import_picocolors9.default.dim(`${segment} โ€บ `)).join("") + import_picocolors9.default.bold(clipped);
16222
+ return head.map((segment) => import_picocolors8.default.dim(`${segment} โ€บ `)).join("") + import_picocolors8.default.bold(clipped);
16279
16223
  }
16280
- function visibleWidth2(text) {
16224
+ function visibleWidth(text) {
16281
16225
  return text.replace(ANSI_PATTERN2, "").length;
16282
16226
  }
16283
16227
  function tabStrip(labels, active, width, { spread = true } = {}) {
@@ -16288,8 +16232,8 @@ function tabStrip(labels, active, width, { spread = true } = {}) {
16288
16232
  for (const [id, label] of labels) {
16289
16233
  const left = Math.floor((cellW - label.length) / 2);
16290
16234
  const cell = " ".repeat(left) + label + " ".repeat(cellW - label.length - left);
16291
- row += id === active ? brand(import_picocolors9.default.bold(cell)) : import_picocolors9.default.dim(cell);
16292
- rule += id === active ? brand("โ”".repeat(cellW)) : import_picocolors9.default.dim("โ”€".repeat(cellW));
16235
+ row += id === active ? brand(import_picocolors8.default.bold(cell)) : import_picocolors8.default.dim(cell);
16236
+ rule += id === active ? brand("โ”".repeat(cellW)) : import_picocolors8.default.dim("โ”€".repeat(cellW));
16293
16237
  }
16294
16238
  return [row, rule];
16295
16239
  }
@@ -16312,18 +16256,18 @@ function tabStrip(labels, active, width, { spread = true } = {}) {
16312
16256
  if (id === active) {
16313
16257
  activeStart = col;
16314
16258
  activeLen = label.length;
16315
- row += brand(import_picocolors9.default.bold(label));
16259
+ row += brand(import_picocolors8.default.bold(label));
16316
16260
  } else {
16317
- row += import_picocolors9.default.dim(label);
16261
+ row += import_picocolors8.default.dim(label);
16318
16262
  }
16319
16263
  col += label.length;
16320
16264
  });
16321
16265
  const tail = Math.max(0, width - activeStart - activeLen);
16322
- const rule = import_picocolors9.default.dim("โ”€".repeat(activeStart)) + brand("โ”".repeat(activeLen)) + import_picocolors9.default.dim("โ”€".repeat(tail));
16266
+ const rule = import_picocolors8.default.dim("โ”€".repeat(activeStart)) + brand("โ”".repeat(activeLen)) + import_picocolors8.default.dim("โ”€".repeat(tail));
16323
16267
  return [row, rule];
16324
16268
  }
16325
16269
  function centerBlock(lines, width) {
16326
- const blockWidth = Math.max(...lines.map(visibleWidth2));
16270
+ const blockWidth = Math.max(...lines.map(visibleWidth));
16327
16271
  const pad = " ".repeat(Math.max(0, Math.floor((width - blockWidth) / 2)));
16328
16272
  return lines.map((line) => pad + line);
16329
16273
  }
@@ -16378,10 +16322,10 @@ function installCenteredLayout(stream = process.stdout) {
16378
16322
  active = false;
16379
16323
  };
16380
16324
  }
16381
- var import_picocolors9, ESC2, CONTENT_WIDTH = 64, TOKEN_PATTERN, COLUMN_RESET_FINALS, active = false, ANSI_PATTERN2, TAB_CELL_PAD = 2;
16325
+ var import_picocolors8, ESC2, CONTENT_WIDTH = 64, TOKEN_PATTERN, COLUMN_RESET_FINALS, active = false, ANSI_PATTERN2, TAB_CELL_PAD = 2;
16382
16326
  var init_layout = __esm(() => {
16383
16327
  init_styles();
16384
- import_picocolors9 = __toESM(require_picocolors(), 1);
16328
+ import_picocolors8 = __toESM(require_picocolors(), 1);
16385
16329
  ESC2 = String.fromCharCode(27);
16386
16330
  TOKEN_PATTERN = new RegExp(`(\r
16387
16331
  |\r|
@@ -16390,6 +16334,73 @@ var init_layout = __esm(() => {
16390
16334
  ANSI_PATTERN2 = new RegExp(`${ESC2}\\[[0-9;?]*[A-Za-z]`, "g");
16391
16335
  });
16392
16336
 
16337
+ // src/setup/styles.ts
16338
+ function hasTruecolor() {
16339
+ return /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
16340
+ }
16341
+ function brand(msg) {
16342
+ if (!import_picocolors9.default.isColorSupported)
16343
+ return msg;
16344
+ return hasTruecolor() ? `${BRAND_FG}${msg}${RESET_FG}` : import_picocolors9.default.green(msg);
16345
+ }
16346
+ function brandBadge(msg) {
16347
+ if (!import_picocolors9.default.isColorSupported)
16348
+ return `[ ${msg} ]`;
16349
+ const label = import_picocolors9.default.bold(` ${msg} `);
16350
+ return hasTruecolor() ? `${BRAND_BG}${INK_FG}${label}${RESET_FG}${RESET_BG}` : import_picocolors9.default.bgGreen(import_picocolors9.default.black(label));
16351
+ }
16352
+ function dim(msg) {
16353
+ return import_picocolors9.default.dim(msg);
16354
+ }
16355
+ function formatSetupSummary(title, items, marker = IconAdd) {
16356
+ const styled = marker === IconRemove ? import_picocolors9.default.red(marker) : brand(marker);
16357
+ const lines = items.map((item) => {
16358
+ const path = `${item.path}${item.status ? ` (${item.status})` : ""}`;
16359
+ return item.label ? `${styled} ${item.label}
16360
+ ${dim(path)}` : `${styled} ${dim(path)}`;
16361
+ });
16362
+ return `${title}
16363
+ ${lines.join(`
16364
+ `)}`;
16365
+ }
16366
+ function browserFallbackHint(url) {
16367
+ return dim(`If your browser doesn't open automatically, visit:
16368
+ ${url}`);
16369
+ }
16370
+ function info(msg) {
16371
+ return import_picocolors9.default.cyan(msg);
16372
+ }
16373
+ function wrapLog(text, columns = process.stdout.columns || 80) {
16374
+ const width = Math.max(20, contentWidth(columns) - LOG_GUTTER);
16375
+ const out = [];
16376
+ for (const paragraph of text.split(`
16377
+ `)) {
16378
+ if (visibleWidth(paragraph) <= width) {
16379
+ out.push(paragraph);
16380
+ continue;
16381
+ }
16382
+ let line = "";
16383
+ for (const word of paragraph.split(" ")) {
16384
+ if (line === "") {
16385
+ line = word;
16386
+ } else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
16387
+ line = `${line} ${word}`;
16388
+ } else {
16389
+ out.push(line);
16390
+ line = word;
16391
+ }
16392
+ }
16393
+ out.push(line);
16394
+ }
16395
+ return out.join(`
16396
+ `);
16397
+ }
16398
+ var import_picocolors9, IconAdd = "โœ”", IconRemove = "-", BRAND_FG = "\x1B[38;2;82;164;15m", BRAND_BG = "\x1B[48;2;82;164;15m", INK_FG = "\x1B[38;2;14;14;14m", RESET_FG = "\x1B[39m", RESET_BG = "\x1B[49m", LOG_GUTTER = 4;
16399
+ var init_styles = __esm(() => {
16400
+ init_layout();
16401
+ import_picocolors9 = __toESM(require_picocolors(), 1);
16402
+ });
16403
+
16393
16404
  // src/tui/menu.ts
16394
16405
  function parseKeys(chunk) {
16395
16406
  const keys = [];
@@ -16427,9 +16438,9 @@ function reduceMenuKey(key, selected, count) {
16427
16438
  return { type: "none" };
16428
16439
  }
16429
16440
  function renderMenuFrame(message, options, selected) {
16430
- const labelWidth = Math.max(...options.map((option) => visibleWidth2(option.label)));
16441
+ const labelWidth = Math.max(...options.map((option) => visibleWidth(option.label)));
16431
16442
  const rows = options.map((option, index) => {
16432
- const label = option.label + " ".repeat(labelWidth - visibleWidth2(option.label));
16443
+ const label = option.label + " ".repeat(labelWidth - visibleWidth(option.label));
16433
16444
  const hint = option.hint ? ` ${import_picocolors10.default.dim(option.hint)}` : "";
16434
16445
  return index === selected ? `${brand(POINTER)} ${import_picocolors10.default.bold(label)}${hint}` : ` ${import_picocolors10.default.dim(label)}${hint}`;
16435
16446
  });
@@ -18346,7 +18357,7 @@ async function stepConnectGitHubRepo(cfg, detected = detectGitRepo(), opts = {})
18346
18357
  }
18347
18358
  repos = refresh.repos;
18348
18359
  if (!refresh.foundNew) {
18349
- log2.warn("GitHub may still be syncing. Pick 'Refresh list' in a moment to re-check; sync usually completes within a minute.");
18360
+ log2.warn(wrapLog("GitHub may still be syncing. Pick 'Refresh list' in a moment to re-check; sync usually completes within a minute."));
18350
18361
  }
18351
18362
  continue;
18352
18363
  }
@@ -21945,7 +21956,8 @@ async function runKnowledgeSync(options = {}) {
21945
21956
  offRecord = gate.ready.filter((s) => agentOff(s) && isUnanswered(s, state.sessions[sessionKey(s)]));
21946
21957
  const gatedReady = gate.ready.filter((s) => !agentOff(s));
21947
21958
  const gatedOpen = gate.open.filter((s) => !agentOff(s));
21948
- const inScope = (sessions) => filterSessionsByRepo(sessions, repoFilter, (s) => locator.resolveRepo(s));
21959
+ const scope = options.everyRepo && !deps.ship ? null : repoFilter;
21960
+ const inScope = (sessions) => filterSessionsByRepo(sessions, scope, (s) => locator.resolveRepo(s));
21949
21961
  ready = inScope(holdBack === "backoff" ? gatedReady.filter(isEnded) : gatedReady);
21950
21962
  open = inScope(gatedOpen);
21951
21963
  if (holdBack === "backoff")
@@ -22019,22 +22031,47 @@ async function runKnowledgeSync(options = {}) {
22019
22031
  failed: 0
22020
22032
  };
22021
22033
  const rank = (s) => ended.some((e) => namesSession(e, s)) ? 0 : isEnded(s) ? 1 : 2;
22022
- const ordered = [...todo].sort((a, b) => rank(a) - rank(b) || Date.parse(a.updated) - Date.parse(b.updated));
22023
- const batch = ordered.slice(0, Math.max(SHIP_BATCH_LIMIT, ordered.filter(isEnded).length));
22024
- logger.debug("sync", `shipping ${batch.length} of ${todo.length} ready sessions`);
22034
+ const batch = [...todo].sort((a, b) => rank(a) - rank(b) || Date.parse(a.updated) - Date.parse(b.updated));
22035
+ logger.debug("sync", `shipping ${batch.length} ready sessions`);
22025
22036
  const hold = (s) => {
22026
22037
  const current = loadState();
22027
22038
  if (!isShippingEnabled(current))
22028
22039
  return "stop";
22029
22040
  return isAgentIncognito(current, s, lineage) ? "agent" : null;
22030
22041
  };
22042
+ const saved = new Map;
22043
+ const reported = [];
22044
+ let savedShipped = 0;
22045
+ const settle = (result) => {
22046
+ reported.push(result);
22047
+ lock.refresh?.();
22048
+ const key = sessionKey(result.session);
22049
+ const entry = ledgerEntry(result, locked.sessions[key], now().toISOString(), cliVersion);
22050
+ if (!entry)
22051
+ return;
22052
+ saved.set(key, entry);
22053
+ try {
22054
+ const counted = result.outcome === "shipped" && !result.session.parentId ? 1 : 0;
22055
+ const next = loadState();
22056
+ next.sessions[key] = entry;
22057
+ next.total_shipped = (next.total_shipped ?? 0) + counted;
22058
+ next.run = run;
22059
+ saveState(next);
22060
+ savedShipped += counted;
22061
+ } catch {}
22062
+ };
22031
22063
  let results = [];
22032
22064
  if (batch.length > 0) {
22033
22065
  try {
22034
- results = await deps.ship(batch, (s) => shippedPrefix(locked.sessions[sessionKey(s)]), hold);
22066
+ results = await deps.ship(batch, (s) => shippedPrefix(locked.sessions[sessionKey(s)]), hold, settle);
22035
22067
  } catch (err) {
22036
22068
  const message = err instanceof Error ? err.message : String(err);
22037
- results = [{ session: batch[0], outcome: "failed", message }];
22069
+ const answered = new Set(reported.map((r) => sessionKey(r.session)));
22070
+ const next = batch.find((s) => !answered.has(sessionKey(s)));
22071
+ results = [
22072
+ ...reported,
22073
+ ...next ? [{ session: next, outcome: "failed", message }] : []
22074
+ ];
22038
22075
  }
22039
22076
  }
22040
22077
  const resultOf = new Map(results.map((r) => [sessionKey(r.session), r]));
@@ -22050,7 +22087,7 @@ async function runKnowledgeSync(options = {}) {
22050
22087
  counts[result.outcome] += 1;
22051
22088
  else
22052
22089
  counts.subagents[result.outcome] += 1;
22053
- const entry = ledgerEntry(result, locked.sessions[key], at, cliVersion);
22090
+ const entry = saved.get(key) ?? ledgerEntry(result, locked.sessions[key], at, cliVersion);
22054
22091
  if (!entry) {
22055
22092
  error = result.message ?? "unknown error";
22056
22093
  logger.debug("sync", `shipping failed at ${key}: ${error}`);
@@ -22077,7 +22114,7 @@ async function runKnowledgeSync(options = {}) {
22077
22114
  if (batch.length > 0) {
22078
22115
  next.last_attempt_at = at;
22079
22116
  next.consecutive_failures = counts.failed > 0 ? next.consecutive_failures + 1 : 0;
22080
- next.total_shipped = (next.total_shipped ?? 0) + counts.shipped;
22117
+ next.total_shipped = (next.total_shipped ?? 0) + counts.shipped - savedShipped;
22081
22118
  next.run = run;
22082
22119
  }
22083
22120
  if (resumes)
@@ -22097,7 +22134,7 @@ async function runKnowledgeSync(options = {}) {
22097
22134
  lock.release();
22098
22135
  }
22099
22136
  }
22100
- var SCAN_WINDOW_DAYS = 30, LEDGER_GRACE_DAYS = 7, SHIP_BATCH_LIMIT = 20, DAY_MS, ENDED_LOCK_WAIT_MS, LOCK_POLL_MS = 2000, LOG_PREVIEW_LIMIT = 10;
22137
+ var SCAN_WINDOW_DAYS = 30, LEDGER_GRACE_DAYS = 7, DAY_MS, ENDED_LOCK_WAIT_MS, LOCK_POLL_MS = 2000, LOG_PREVIEW_LIMIT = 10;
22101
22138
  var init_sync = __esm(() => {
22102
22139
  init_logger();
22103
22140
  init_lineage();
@@ -22178,7 +22215,7 @@ var init_backlog = __esm(() => {
22178
22215
  });
22179
22216
 
22180
22217
  // src/telemetry/debug-id.ts
22181
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "32ee4cf3-5398-61c9-6475-6e2164756e21";
22218
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "b28c8070-bfe4-35bd-6475-6e2164756e21";
22182
22219
 
22183
22220
  // src/telemetry/telemetry.ts
22184
22221
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -23147,10 +23184,6 @@ function isDirectory(path) {
23147
23184
  return false;
23148
23185
  }
23149
23186
  }
23150
- function saveScope(repos) {
23151
- const { repo_filter: _repos, project_filter: _folders, ...rest } = loadSyncState();
23152
- saveSyncState(repos ? { ...rest, repo_filter: repos } : rest);
23153
- }
23154
23187
  function scopeCommand() {
23155
23188
  const cmd = new Command("scope").description("Show or set which repos' sessions ship to Dosu memory (the TUI's sync scope)");
23156
23189
  cmd.command("show").description("Show the repos whose sessions ship").option("--json", "Output as JSON").action((opts) => {
@@ -23180,11 +23213,11 @@ function scopeCommand() {
23180
23213
  repos.add(repo);
23181
23214
  }
23182
23215
  const sorted = [...repos].sort();
23183
- saveScope(sorted);
23216
+ saveRepoFilter(sorted);
23184
23217
  console.log(`โœ“ Sessions ship only from ${sorted.join(", ")}.`);
23185
23218
  });
23186
23219
  cmd.command("clear").description("Ship sessions from every repo, and sessions outside any repo").action(() => {
23187
- saveScope(null);
23220
+ saveRepoFilter(null);
23188
23221
  console.log("โœ“ Sessions from every repo ship, and sessions outside any repo.");
23189
23222
  });
23190
23223
  return cmd;
@@ -23598,7 +23631,7 @@ function createShipStep(options) {
23598
23631
  }
23599
23632
  return async (sessions, shippedOf = () => {
23600
23633
  return;
23601
- }, hold = () => null) => {
23634
+ }, hold = () => null, settled = () => {}) => {
23602
23635
  const resolver = options.resolveProject && options.resolveBranch ? undefined : createProjectDirResolver();
23603
23636
  const resolve = {
23604
23637
  resolveProject: options.resolveProject ?? ((session) => resolver?.resolveProject(session) ?? null),
@@ -23610,6 +23643,7 @@ function createShipStep(options) {
23610
23643
  if (!result)
23611
23644
  break;
23612
23645
  results.push(result);
23646
+ settled(result);
23613
23647
  if (result.outcome === "failed")
23614
23648
  break;
23615
23649
  }
@@ -23999,21 +24033,11 @@ ${results.length - opts.limit} more results not shown.`));
23999
24033
  ...opts.retryRejected ? { retryRejectedBefore: new Date } : {},
24000
24034
  deps
24001
24035
  };
24002
- let outcome = await runKnowledgeSync(syncOptions);
24003
- let sessionsShipped = outcome.counts?.shipped ?? 0;
24004
- if ((opts.bootstrap || opts.flush) && deps.ship) {
24005
- const maxRounds = Math.ceil(outcome.readySessions / SHIP_BATCH_LIMIT) + 2;
24006
- for (let round = 1;outcome.status === "shipped" && (outcome.settledSessions ?? 0) > 0 && (outcome.settledSessions ?? 0) < outcome.readySessions && round < maxRounds; round++) {
24007
- if (!opts.quiet && !opts.json)
24008
- printSyncOutcome(outcome);
24009
- outcome = await runKnowledgeSync(syncOptions);
24010
- sessionsShipped += outcome.counts?.shipped ?? 0;
24011
- }
24012
- }
24036
+ const outcome = await runKnowledgeSync(syncOptions);
24013
24037
  recordCommandFacets({
24014
24038
  sync_trigger: trigger,
24015
24039
  sync_status: outcome.status,
24016
- sessions_shipped: sessionsShipped
24040
+ sessions_shipped: outcome.counts?.shipped ?? 0
24017
24041
  });
24018
24042
  if (opts.quiet)
24019
24043
  return;
@@ -24345,9 +24369,9 @@ function printSyncOutcome(outcome) {
24345
24369
  process.exitCode = 1;
24346
24370
  break;
24347
24371
  }
24348
- const remaining = outcome.readySessions - (outcome.settledSessions ?? 0);
24349
- if (remaining > 0) {
24350
- console.log(import_picocolors23.default.dim(`${remaining} more in the backlog; run sync again to continue.`));
24372
+ if (outcome.inFlightSessions > 0) {
24373
+ const n = outcome.inFlightSessions;
24374
+ console.log(import_picocolors23.default.dim(`${n} session${plural(n)} still in progress will ship once ${n === 1 ? "it ends" : "they end"}.`));
24351
24375
  }
24352
24376
  break;
24353
24377
  }
@@ -29659,8 +29683,8 @@ function confirmBox(queuedCount, backlog, width, mode = "start") {
29659
29683
  "",
29660
29684
  `${import_picocolors37.default.bold("enter")} ${verb} ยท ${import_picocolors37.default.bold("esc")} cancel`
29661
29685
  ];
29662
- const inner = Math.min(maxInner, Math.max(...rows.map(visibleWidth2)));
29663
- const pad = (row) => row + " ".repeat(Math.max(0, inner - visibleWidth2(row)));
29686
+ const inner = Math.min(maxInner, Math.max(...rows.map(visibleWidth)));
29687
+ const pad = (row) => row + " ".repeat(Math.max(0, inner - visibleWidth(row)));
29664
29688
  return [
29665
29689
  import_picocolors37.default.dim(`โ•ญ${"โ”€".repeat(inner + 2)}โ•ฎ`),
29666
29690
  ...rows.map((row) => `${import_picocolors37.default.dim("โ”‚")} ${pad(row)} ${import_picocolors37.default.dim("โ”‚")}`),
@@ -30566,6 +30590,58 @@ var init_rules_step = __esm(() => {
30566
30590
  init_styles();
30567
30591
  });
30568
30592
 
30593
+ // src/setup/scope-step.ts
30594
+ function scopeCandidates(sessions, current) {
30595
+ const byRepo = new Map;
30596
+ for (const { repo, updated } of sessions) {
30597
+ if (!repo)
30598
+ continue;
30599
+ const candidate = byRepo.get(repo);
30600
+ if (!candidate)
30601
+ byRepo.set(repo, { repo, sessions: 1, updated });
30602
+ else {
30603
+ candidate.sessions += 1;
30604
+ if (updated > candidate.updated)
30605
+ candidate.updated = updated;
30606
+ }
30607
+ }
30608
+ for (const repo of current ?? []) {
30609
+ if (!byRepo.has(repo))
30610
+ byRepo.set(repo, { repo, sessions: 0, updated: "" });
30611
+ }
30612
+ return [...byRepo.values()].sort((a, b) => b.sessions - a.sessions || b.updated.localeCompare(a.updated) || a.repo.localeCompare(b.repo));
30613
+ }
30614
+ function canPickScope(candidates) {
30615
+ return process.stdin.isTTY === true && candidates.length > 1;
30616
+ }
30617
+ async function stepPickSyncScope(candidates, current) {
30618
+ const repos = candidates.map((candidate) => candidate.repo);
30619
+ const plural = (n) => n === 1 ? "" : "s";
30620
+ const selected = await multiselect2({
30621
+ message: "Ship sessions to Dosu memory from which repos?",
30622
+ options: candidates.map(({ repo, sessions }) => ({
30623
+ value: repo,
30624
+ label: displayRepo(repo),
30625
+ hint: sessions > 0 ? `${sessions} session${plural(sessions)}` : "no recent sessions"
30626
+ })),
30627
+ initialValues: current ?? repos,
30628
+ summary: (picked) => picked.length === repos.length ? "all repos ยท new ones included automatically" : `${picked.length} of ${repos.length} repos`,
30629
+ validate: (picked) => picked.length === 0 ? "Select at least one repo." : undefined
30630
+ });
30631
+ if (isCancel2(selected))
30632
+ return current;
30633
+ const scope = selected.length === repos.length ? null : selected;
30634
+ saveRepoFilter(scope);
30635
+ log2.success(`Sync scope ${dim(`ยท ${scope ? scope.map(displayRepo).join(", ") : "all repos"}`)}`);
30636
+ return scope;
30637
+ }
30638
+ var init_scope_step = __esm(() => {
30639
+ init_repo();
30640
+ init_state();
30641
+ init_prompts();
30642
+ init_styles();
30643
+ });
30644
+
30569
30645
  // src/auth/server.ts
30570
30646
  function escapeHtml(value) {
30571
30647
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -31030,7 +31106,7 @@ async function runSetupFlow(opts = {}) {
31030
31106
  return;
31031
31107
  }
31032
31108
  if (!refreshed.finishedOnboarding) {
31033
- log2.warn("Your account still needs onboarding. Finish it in the browser at the Dosu app, then re-run `dosu setup`.");
31109
+ log2.warn(wrapLog("Your account still needs onboarding. Finish it in the browser at the Dosu app, then re-run `dosu setup`."));
31034
31110
  trackInBackground(trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_failed", {
31035
31111
  reason: "onboarding_incomplete_after_handshake"
31036
31112
  }));
@@ -31130,17 +31206,32 @@ async function stepOfferInitialSync(cfg) {
31130
31206
  logger.info("setup", "Step: offer initial knowledge sync");
31131
31207
  const s = spinner2();
31132
31208
  s.start("Checking for agent sessions from the last 30 days...");
31133
- const outcome = await runKnowledgeSync();
31134
- const backlog = outcome.status === "backlog" ? unsettledSessions(outcome.sessions, state) : [];
31209
+ const outcome = await runKnowledgeSync({ everyRepo: true });
31210
+ const storedScope = loadSyncState().repo_filter ?? null;
31211
+ const everyBacklog = outcome.status === "backlog" ? unsettledSessions(outcome.sessions, state) : [];
31135
31212
  const lineage = sessionLineage(outcome.sessions);
31136
- const n = withoutSubagents(backlog).filter((session) => !isAgentIncognito(state, session, lineage)).length;
31213
+ const offered = (sessions) => withoutSubagents(sessions).filter((session) => !isAgentIncognito(state, session, lineage));
31214
+ const inScope = (scope) => filterSessionsByRepo(everyBacklog, scope, (session) => session.repo ?? null);
31215
+ const candidates = scopeCandidates(offered(everyBacklog), storedScope);
31216
+ const pickScope = canPickScope(candidates);
31217
+ let backlog = pickScope ? everyBacklog : inScope(storedScope);
31218
+ let n = offered(backlog).length;
31137
31219
  if (n === 0) {
31138
31220
  s.stop("No recent agent sessions. Dosu memory learns from new ones as you work.");
31139
31221
  recordCommandFacets({ backfill_offer: "not-offered" });
31140
31222
  return;
31141
31223
  }
31142
- const them = n === 1 ? "it" : "them";
31143
31224
  s.stop(`Found ${n} agent session${n === 1 ? "" : "s"} from the last 30 days on this machine.`);
31225
+ if (pickScope) {
31226
+ backlog = inScope(await stepPickSyncScope(candidates, storedScope));
31227
+ n = offered(backlog).length;
31228
+ if (n === 0) {
31229
+ log2.info(wrapLog("No sessions from those repos are waiting. Dosu memory learns from new ones."));
31230
+ recordCommandFacets({ backfill_offer: "not-offered" });
31231
+ return;
31232
+ }
31233
+ }
31234
+ const them = n === 1 ? "it" : "them";
31144
31235
  const incognito = installedIncognitoCommands();
31145
31236
  log2.message(wrapLog([
31146
31237
  "Dosu memory learns from finished agent sessions: where things live, commands that work, approaches that failed, so the next agent starts from what the last one learned.",
@@ -31922,6 +32013,7 @@ var init_flow2 = __esm(() => {
31922
32013
  init_agents_md_step();
31923
32014
  init_analytics2();
31924
32015
  init_rules_step();
32016
+ init_scope_step();
31925
32017
  init_styles();
31926
32018
  import_picocolors38 = __toESM(require_picocolors(), 1);
31927
32019
  OAUTH_ANALYTICS_REASONS = new Set([
@@ -32406,9 +32498,9 @@ function styleHint(hint) {
32406
32498
  return [before && import_picocolors41.default.dim(before), import_picocolors41.default.cyan(command), after && import_picocolors41.default.dim(after)].filter(Boolean).join("");
32407
32499
  }
32408
32500
  function framedNotice(content, width) {
32409
- const innerWidth = Math.max(...content.map(visibleWidth2)) + BOX_PADDING * 2;
32501
+ const innerWidth = Math.max(...content.map(visibleWidth)) + BOX_PADDING * 2;
32410
32502
  const framed = (line) => {
32411
- const pad = innerWidth - visibleWidth2(line);
32503
+ const pad = innerWidth - visibleWidth(line);
32412
32504
  const left = Math.floor(pad / 2);
32413
32505
  return `${import_picocolors41.default.yellow("โ”‚")}${" ".repeat(left)}${line}${" ".repeat(pad - left)}${import_picocolors41.default.yellow("โ”‚")}`;
32414
32506
  };
@@ -32826,7 +32918,7 @@ function wrapValue(text, room) {
32826
32918
  const append = (piece, sep) => {
32827
32919
  if (current === "")
32828
32920
  current = piece;
32829
- else if (visibleWidth2(current) + sep.length + visibleWidth2(piece) <= room) {
32921
+ else if (visibleWidth(current) + sep.length + visibleWidth(piece) <= room) {
32830
32922
  current += sep + piece;
32831
32923
  } else {
32832
32924
  lines.push(current);
@@ -32834,7 +32926,7 @@ function wrapValue(text, room) {
32834
32926
  }
32835
32927
  };
32836
32928
  for (const item of text.split(LIST_SEP)) {
32837
- const pieces = visibleWidth2(item) <= room ? [item] : item.split(" ");
32929
+ const pieces = visibleWidth(item) <= room ? [item] : item.split(" ");
32838
32930
  pieces.forEach((piece, i) => {
32839
32931
  append(piece, i === 0 ? LIST_SEP : " ");
32840
32932
  });
@@ -33534,43 +33626,27 @@ async function runSettings(cfg) {
33534
33626
  }
33535
33627
  }
33536
33628
  function discoverSessionRepos(current) {
33537
- const counts = new Map;
33629
+ const inRepos = [];
33538
33630
  try {
33539
33631
  const resolver = createProjectDirResolver();
33540
33632
  for (const session of scanAgentSessions({})) {
33541
33633
  const repo = resolver.resolveRepo(session);
33542
33634
  if (repo)
33543
- counts.set(repo, (counts.get(repo) ?? 0) + 1);
33635
+ inRepos.push({ ...session, repo });
33544
33636
  }
33545
33637
  resolver.flush();
33546
33638
  } catch {}
33547
- for (const repo of current ?? [])
33548
- if (!counts.has(repo))
33549
- counts.set(repo, 0);
33550
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([repo]) => repo);
33639
+ return scopeCandidates(inRepos, current);
33551
33640
  }
33552
33641
  async function runStudyingProjectsSetting() {
33553
33642
  const state = loadSyncState();
33554
33643
  const current = studyRepoFilter(state, () => scanAgentSessions({}), createProjectDirResolver());
33555
- const repos = discoverSessionRepos(current);
33556
- if (repos.length === 0) {
33644
+ const candidates = discoverSessionRepos(current);
33645
+ if (candidates.length === 0) {
33557
33646
  log2.info("No agent sessions inside a git repo found yet; nothing to scope.");
33558
33647
  return;
33559
33648
  }
33560
- const selected = await multiselect2({
33561
- message: "Ship sessions to Dosu memory from which repos?",
33562
- options: repos.map((repo) => ({ label: displayRepo(repo), hint: repo, value: repo })),
33563
- initialValues: current ?? repos,
33564
- summary: (picked) => picked.length === repos.length ? "all repos ยท new ones included automatically" : `${picked.length} of ${repos.length} repos`,
33565
- validate: (picked) => picked.length === 0 ? "Select at least one repo." : undefined
33566
- });
33567
- if (isCancel2(selected))
33568
- return;
33569
- const { repo_filter: _repos, project_filter: _folders, ...fresh } = loadSyncState();
33570
- const all = selected.length === repos.length;
33571
- saveSyncState(all ? fresh : { ...fresh, repo_filter: [...selected] });
33572
- const scope = all ? "all repos" : selected.map(displayRepo).join(", ");
33573
- log2.success(`Sync scope ${dim(`ยท ${scope}`)}`);
33649
+ await stepPickSyncScope(candidates, current);
33574
33650
  }
33575
33651
  async function handleAuthenticate(cfg) {
33576
33652
  const shouldLogin = await confirm2({ message: "Open browser to log in?" });
@@ -33626,6 +33702,7 @@ var init_tui = __esm(() => {
33626
33702
  init_scan();
33627
33703
  init_agents_md_step();
33628
33704
  init_flow2();
33705
+ init_scope_step();
33629
33706
  init_styles();
33630
33707
  init_state();
33631
33708
  init_status();
@@ -34900,4 +34977,4 @@ main().catch((err) => {
34900
34977
  process.exit(1);
34901
34978
  });
34902
34979
 
34903
- //# debugId=32ee4cf3-5398-61c9-6475-6e2164756e21
34980
+ //# debugId=b28c8070-bfe4-35bd-6475-6e2164756e21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.67.0-beta.1",
3
+ "version": "0.67.0-beta.3",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Set up Dosu MCP for AI tools and work with Dosu Libraries, docs, and reviews",
6
6
  "license": "MIT",