@dosu/cli 0.18.0 → 0.19.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 (2) hide show
  1. package/bin/dosu.js +367 -153
  2. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -2083,7 +2083,7 @@ var init_esm = __esm(() => {
2083
2083
  });
2084
2084
 
2085
2085
  // src/config/config.ts
2086
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2086
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2087
2087
  import { join } from "node:path";
2088
2088
  function getConfigDir() {
2089
2089
  const dirName = process.env.DOSU_DEV === "true" ? "dosu-cli-dev" : "dosu-cli";
@@ -2118,7 +2118,9 @@ function saveConfig(cfg) {
2118
2118
  if (!existsSync(dir)) {
2119
2119
  mkdirSync(dir, { recursive: true, mode: 448 });
2120
2120
  }
2121
- writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 384 });
2121
+ const tmp = `${path}.${process.pid}.tmp`;
2122
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2), { mode: 384 });
2123
+ renameSync(tmp, path);
2122
2124
  }
2123
2125
  function isAuthenticated(cfg) {
2124
2126
  return cfg.access_token !== "";
@@ -2247,9 +2249,30 @@ class Client {
2247
2249
  }
2248
2250
  }
2249
2251
  async refreshToken() {
2252
+ this.adoptNewerDiskTokens();
2250
2253
  if (!this.config.refresh_token) {
2251
2254
  throw new Error("no refresh token available");
2252
2255
  }
2256
+ try {
2257
+ await this.refreshTokenOnce();
2258
+ } catch (err) {
2259
+ if (!this.adoptNewerDiskTokens()) {
2260
+ throw err;
2261
+ }
2262
+ await this.refreshTokenOnce();
2263
+ }
2264
+ }
2265
+ adoptNewerDiskTokens() {
2266
+ const disk = loadConfig();
2267
+ if (!disk.refresh_token || disk.refresh_token === this.config.refresh_token) {
2268
+ return false;
2269
+ }
2270
+ this.config.access_token = disk.access_token;
2271
+ this.config.refresh_token = disk.refresh_token;
2272
+ this.config.expires_at = disk.expires_at;
2273
+ return true;
2274
+ }
2275
+ async refreshTokenOnce() {
2253
2276
  const supabaseURL = getSupabaseURL();
2254
2277
  const endpoint = `${supabaseURL}/auth/v1/token?grant_type=refresh_token`;
2255
2278
  const resp = await fetch(endpoint, {
@@ -4525,7 +4548,7 @@ var init_dist4 = __esm(() => {
4525
4548
  function getVersionString() {
4526
4549
  return `v${VERSION}`;
4527
4550
  }
4528
- var VERSION = "0.18.0";
4551
+ var VERSION = "0.19.0";
4529
4552
 
4530
4553
  // src/debug/logger.ts
4531
4554
  import {
@@ -5421,6 +5444,45 @@ var init_dist6 = __esm(() => {
5421
5444
  J2 = `${import_picocolors7.default.gray(o)} `;
5422
5445
  });
5423
5446
 
5447
+ // src/hooks/prompts.ts
5448
+ var exports_prompts = {};
5449
+ __export(exports_prompts, {
5450
+ buildReadyEnvelope: () => buildReadyEnvelope,
5451
+ STOP_PREFIX: () => STOP_PREFIX,
5452
+ SAVE_NUDGE: () => SAVE_NUDGE,
5453
+ LOOKUP_STARTED_NOTE: () => LOOKUP_STARTED_NOTE
5454
+ });
5455
+ function buildReadyEnvelope(context, saveRecommended = false) {
5456
+ const trimmed = context.trim();
5457
+ const blocks = [];
5458
+ if (trimmed) {
5459
+ blocks.push([
5460
+ "Dosu knowledge context for this task:",
5461
+ "",
5462
+ trimmed,
5463
+ "",
5464
+ "How to use this:",
5465
+ "- Use it quietly to answer faster and with fewer broad searches. Keep working normally.",
5466
+ "- This is the known likely path, not the full boundary. Before finalizing, verify adjacent " + "public/API entrypoints and edge cases that this route map may not cover.",
5467
+ "- Attribution: this context is your team's existing knowledge (docs, past PRs, discussions) " + "surfaced by Dosu — not the live code you're reading. For each main finding it speaks to, " + "add one brief cited note on how the team's knowledge relates: if it agrees, note it in a " + "line and cite the source; if it differs, flag it as worth confirming and reconcile before " + "finalizing; if it adds something the code alone wouldn't tell you (a rationale, a gotcha, " + "a past decision), surface it, cited.",
5468
+ "- Surface this at each main finding the context speaks to — but skip the ones it is " + "genuinely silent on, and never invent agreement or disagreement. Keep each note brief and " + 'next to the finding; do not open with "Dosu told me…", do not add a closing praise ' + "paragraph, and do not reference any ticket or lookup id."
5469
+ ].join(`
5470
+ `));
5471
+ }
5472
+ if (saveRecommended) {
5473
+ blocks.push(SAVE_NUDGE);
5474
+ }
5475
+ return blocks.join(`
5476
+
5477
+ `);
5478
+ }
5479
+ var LOOKUP_STARTED_NOTE, STOP_PREFIX, SAVE_NUDGE;
5480
+ var init_prompts = __esm(() => {
5481
+ LOOKUP_STARTED_NOTE = "Dosu started a background knowledge lookup for this request. Keep working normally — " + "do not wait or pause for it. If Dosu context arrives during this session, fold it in " + "quietly to work faster and avoid wrong assumptions. Mention Dosu when its context " + "helps you.";
5482
+ STOP_PREFIX = "Dosu knowledge finished after your last action. Re-check your current conclusion against " + "it, then continue or finish — do not redo work it merely confirms.";
5483
+ SAVE_NUDGE = "Dosu has little or no existing knowledge on this topic. If you uncover something durable — " + "a non-obvious mechanism, a gotcha, or a route through the code — save it with the Dosu " + "`save_topic` tool when you finish, so future sessions start ahead. Only save genuinely " + "reusable knowledge; skip trivial or one-off details.";
5484
+ });
5485
+
5424
5486
  // src/hooks/ticket-client.ts
5425
5487
  var exports_ticket_client = {};
5426
5488
  __export(exports_ticket_client, {
@@ -5471,45 +5533,6 @@ var init_ticket_client = __esm(() => {
5471
5533
  };
5472
5534
  });
5473
5535
 
5474
- // src/hooks/prompts.ts
5475
- var exports_prompts = {};
5476
- __export(exports_prompts, {
5477
- buildReadyEnvelope: () => buildReadyEnvelope,
5478
- STOP_PREFIX: () => STOP_PREFIX,
5479
- SAVE_NUDGE: () => SAVE_NUDGE,
5480
- LOOKUP_STARTED_NOTE: () => LOOKUP_STARTED_NOTE
5481
- });
5482
- function buildReadyEnvelope(context, saveRecommended = false) {
5483
- const trimmed = context.trim();
5484
- const blocks = [];
5485
- if (trimmed) {
5486
- blocks.push([
5487
- "Dosu knowledge context for this task:",
5488
- "",
5489
- trimmed,
5490
- "",
5491
- "How to use this:",
5492
- "- Use it quietly to answer faster and with fewer broad searches. Keep working normally.",
5493
- "- This is the known likely path, not the full boundary. Before finalizing, verify adjacent " + "public/API entrypoints and edge cases that this route map may not cover.",
5494
- "- Attribution: this context is your team's existing knowledge (docs, past PRs, discussions) " + "surfaced by Dosu — not the live code you're reading. For each main finding it speaks to, " + "add one brief cited note on how the team's knowledge relates: if it agrees, note it in a " + "line and cite the source; if it differs, flag it as worth confirming and reconcile before " + "finalizing; if it adds something the code alone wouldn't tell you (a rationale, a gotcha, " + "a past decision), surface it, cited.",
5495
- "- Surface this at each main finding the context speaks to — but skip the ones it is " + "genuinely silent on, and never invent agreement or disagreement. Keep each note brief and " + 'next to the finding; do not open with "Dosu told me…", do not add a closing praise ' + "paragraph, and do not reference any ticket or lookup id."
5496
- ].join(`
5497
- `));
5498
- }
5499
- if (saveRecommended) {
5500
- blocks.push(SAVE_NUDGE);
5501
- }
5502
- return blocks.join(`
5503
-
5504
- `);
5505
- }
5506
- var LOOKUP_STARTED_NOTE, STOP_PREFIX, SAVE_NUDGE;
5507
- var init_prompts = __esm(() => {
5508
- LOOKUP_STARTED_NOTE = "Dosu started a background knowledge lookup for this request. Keep working normally — " + "do not wait or pause for it. If Dosu context arrives during this session, fold it in " + "quietly to work faster and avoid wrong assumptions. Mention Dosu when its context " + "helps you.";
5509
- STOP_PREFIX = "Dosu knowledge finished after your last action. Re-check your current conclusion against " + "it, then continue or finish — do not redo work it merely confirms.";
5510
- SAVE_NUDGE = "Dosu has little or no existing knowledge on this topic. If you uncover something durable — " + "a non-obvious mechanism, a gotcha, or a route through the code — save it with the Dosu " + "`save_topic` tool when you finish, so future sessions start ahead. Only save genuinely " + "reusable knowledge; skip trivial or one-off details.";
5511
- });
5512
-
5513
5536
  // src/agent/output.ts
5514
5537
  var exports_output = {};
5515
5538
  __export(exports_output, {
@@ -6321,6 +6344,135 @@ var init_claude_code = __esm(() => {
6321
6344
  };
6322
6345
  });
6323
6346
 
6347
+ // src/hooks/codex.ts
6348
+ var exports_codex = {};
6349
+ __export(exports_codex, {
6350
+ removeCodexHooks: () => removeCodexHooks,
6351
+ installCodexHooks: () => installCodexHooks,
6352
+ inspectCodexHooks: () => inspectCodexHooks,
6353
+ codexHooksPath: () => codexHooksPath,
6354
+ codexHookCommand: () => codexHookCommand
6355
+ });
6356
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
6357
+ import { join as join5 } from "node:path";
6358
+ function codexHooksPath(dir) {
6359
+ return join5(dir, ".codex", "hooks.json");
6360
+ }
6361
+ function codexHookCommand(subcommand) {
6362
+ return `dosu hooks ${subcommand} --agent codex`;
6363
+ }
6364
+ function dosuGroup2(event) {
6365
+ return {
6366
+ hooks: [
6367
+ {
6368
+ type: "command",
6369
+ command: codexHookCommand(EVENT_SUBCOMMAND2[event]),
6370
+ timeout: EVENT_TIMEOUT_SEC[event],
6371
+ statusMessage: EVENT_STATUS_MESSAGE[event]
6372
+ }
6373
+ ]
6374
+ };
6375
+ }
6376
+ function readHooksFileOrThrow(path) {
6377
+ if (!existsSync6(path))
6378
+ return {};
6379
+ const raw = readFileSync7(path, "utf-8").trim();
6380
+ if (!raw)
6381
+ return {};
6382
+ try {
6383
+ return JSON.parse(raw);
6384
+ } catch {
6385
+ throw new Error(`refusing to modify ${path}: file exists but is not valid JSON`);
6386
+ }
6387
+ }
6388
+ function installCodexHooks(configPath, opts = {}) {
6389
+ const config = readHooksFileOrThrow(configPath);
6390
+ const events = DEFAULT_HOOK_EVENTS.filter((e2) => e2 !== "Stop" || opts.stop !== false);
6391
+ const hooks = typeof config.hooks === "object" && config.hooks ? config.hooks : {};
6392
+ for (const event of events) {
6393
+ const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
6394
+ const kept = groups.filter((g2) => !isDosuGroup(g2));
6395
+ kept.push(dosuGroup2(event));
6396
+ hooks[event] = kept;
6397
+ }
6398
+ for (const event of DEFAULT_HOOK_EVENTS) {
6399
+ if (!events.includes(event) && Array.isArray(hooks[event])) {
6400
+ const kept = hooks[event].filter((g2) => !isDosuGroup(g2));
6401
+ if (kept.length > 0)
6402
+ hooks[event] = kept;
6403
+ else
6404
+ delete hooks[event];
6405
+ }
6406
+ }
6407
+ config.hooks = hooks;
6408
+ saveJSONConfig(configPath, config);
6409
+ return { events: [...events] };
6410
+ }
6411
+ function removeCodexHooks(configPath) {
6412
+ if (!existsSync6(configPath))
6413
+ return { removed: false };
6414
+ let config;
6415
+ try {
6416
+ config = readHooksFileOrThrow(configPath);
6417
+ } catch {
6418
+ return { removed: false };
6419
+ }
6420
+ const hooks = config.hooks;
6421
+ if (typeof hooks !== "object" || !hooks)
6422
+ return { removed: false };
6423
+ let removed = false;
6424
+ for (const event of Object.keys(hooks)) {
6425
+ if (!Array.isArray(hooks[event]))
6426
+ continue;
6427
+ const kept = hooks[event].filter((g2) => !isDosuGroup(g2));
6428
+ if (kept.length !== hooks[event].length) {
6429
+ removed = true;
6430
+ if (kept.length > 0)
6431
+ hooks[event] = kept;
6432
+ else
6433
+ delete hooks[event];
6434
+ }
6435
+ }
6436
+ if (removed)
6437
+ saveJSONConfig(configPath, config);
6438
+ return { removed };
6439
+ }
6440
+ function inspectCodexHooks(configPath) {
6441
+ if (!existsSync6(configPath))
6442
+ return { fileExists: false, events: [] };
6443
+ let config;
6444
+ try {
6445
+ config = readHooksFileOrThrow(configPath);
6446
+ } catch {
6447
+ return { fileExists: true, parseError: true, events: [] };
6448
+ }
6449
+ const hooks = config.hooks;
6450
+ if (typeof hooks !== "object" || !hooks)
6451
+ return { fileExists: true, events: [] };
6452
+ const events = Object.keys(hooks).filter((event) => Array.isArray(hooks[event]) && hooks[event].some((g2) => isDosuGroup(g2)));
6453
+ return { fileExists: true, events };
6454
+ }
6455
+ var EVENT_SUBCOMMAND2, EVENT_TIMEOUT_SEC, EVENT_STATUS_MESSAGE;
6456
+ var init_codex = __esm(() => {
6457
+ init_config_helpers();
6458
+ init_claude_code();
6459
+ EVENT_SUBCOMMAND2 = {
6460
+ UserPromptSubmit: "user-prompt-submit",
6461
+ PostToolUse: "post-tool-use",
6462
+ Stop: "stop"
6463
+ };
6464
+ EVENT_TIMEOUT_SEC = {
6465
+ UserPromptSubmit: 10,
6466
+ PostToolUse: 10,
6467
+ Stop: 30
6468
+ };
6469
+ EVENT_STATUS_MESSAGE = {
6470
+ UserPromptSubmit: "Dosu knowledge lookup",
6471
+ PostToolUse: "Dosu knowledge delivery",
6472
+ Stop: "Dosu final knowledge check"
6473
+ };
6474
+ });
6475
+
6324
6476
  // src/insights/insights.ts
6325
6477
  async function buildInsights({
6326
6478
  client,
@@ -7409,17 +7561,17 @@ var init_insights2 = __esm(() => {
7409
7561
  });
7410
7562
 
7411
7563
  // src/version/skill-update-check.ts
7412
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
7413
- import { join as join5 } from "node:path";
7564
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
7565
+ import { join as join6 } from "node:path";
7414
7566
  function getCachePath() {
7415
- return join5(getConfigDir(), CACHE_FILENAME);
7567
+ return join6(getConfigDir(), CACHE_FILENAME);
7416
7568
  }
7417
7569
  function readSkillCache() {
7418
7570
  try {
7419
7571
  const path = getCachePath();
7420
- if (!existsSync6(path))
7572
+ if (!existsSync7(path))
7421
7573
  return null;
7422
- const data = JSON.parse(readFileSync8(path, "utf-8"));
7574
+ const data = JSON.parse(readFileSync9(path, "utf-8"));
7423
7575
  if (typeof data.lastCheck === "number" && typeof data.latestSha === "string" && typeof data.installedSha === "string") {
7424
7576
  return data;
7425
7577
  }
@@ -7431,7 +7583,7 @@ function readSkillCache() {
7431
7583
  function writeSkillCache(cache) {
7432
7584
  try {
7433
7585
  const dir = getConfigDir();
7434
- if (!existsSync6(dir)) {
7586
+ if (!existsSync7(dir)) {
7435
7587
  mkdirSync5(dir, { recursive: true, mode: 448 });
7436
7588
  }
7437
7589
  writeFileSync4(getCachePath(), JSON.stringify(cache), { mode: 384 });
@@ -7593,21 +7745,21 @@ var init_skill = __esm(() => {
7593
7745
  var MCP_PROVIDER_SLUG = "dosu_mcp";
7594
7746
 
7595
7747
  // src/mcp/detect.ts
7596
- import { existsSync as existsSync7 } from "node:fs";
7748
+ import { existsSync as existsSync8 } from "node:fs";
7597
7749
  import { homedir, platform } from "node:os";
7598
- import { join as join6 } from "node:path";
7750
+ import { join as join7 } from "node:path";
7599
7751
  function isInstalled(paths) {
7600
- return paths.some((p2) => existsSync7(expandHome(p2)));
7752
+ return paths.some((p2) => existsSync8(expandHome(p2)));
7601
7753
  }
7602
7754
  function expandHome(path) {
7603
7755
  if (!path.startsWith("~"))
7604
7756
  return path;
7605
- return join6(homedir(), path.slice(1));
7757
+ return join7(homedir(), path.slice(1));
7606
7758
  }
7607
7759
  function appSupportDir() {
7608
7760
  switch (platform()) {
7609
7761
  case "darwin": {
7610
- return join6(homedir(), "Library", "Application Support");
7762
+ return join7(homedir(), "Library", "Application Support");
7611
7763
  }
7612
7764
  case "win32": {
7613
7765
  return process.env.APPDATA ?? "";
@@ -7616,7 +7768,7 @@ function appSupportDir() {
7616
7768
  const xdg = process.env.XDG_CONFIG_HOME;
7617
7769
  if (xdg)
7618
7770
  return xdg;
7619
- return join6(homedir(), ".config");
7771
+ return join7(homedir(), ".config");
7620
7772
  }
7621
7773
  }
7622
7774
  }
@@ -7697,7 +7849,7 @@ var init_antigravity = __esm(() => {
7697
7849
  });
7698
7850
 
7699
7851
  // src/mcp/providers/claude.ts
7700
- import { join as join7 } from "node:path";
7852
+ import { join as join8 } from "node:path";
7701
7853
  var ClaudeProvider = () => createJSONProvider({
7702
7854
  providerName: "Claude Code",
7703
7855
  providerID: "claude",
@@ -7706,22 +7858,22 @@ var ClaudeProvider = () => createJSONProvider({
7706
7858
  paths: ["~/.claude"],
7707
7859
  globalPath: "~/.claude.json",
7708
7860
  topKey: "mcpServers",
7709
- localConfigPath: (cwd) => join7(cwd, ".mcp.json")
7861
+ localConfigPath: (cwd) => join8(cwd, ".mcp.json")
7710
7862
  });
7711
7863
  var init_claude = __esm(() => {
7712
7864
  init_base();
7713
7865
  });
7714
7866
 
7715
7867
  // src/mcp/providers/claude-desktop.ts
7716
- import { join as join8 } from "node:path";
7868
+ import { join as join9 } from "node:path";
7717
7869
  var ClaudeDesktopProvider = () => ({
7718
7870
  name: () => "Claude Desktop",
7719
7871
  id: () => "claude-desktop",
7720
7872
  supportsLocal: () => false,
7721
7873
  priority: () => 2,
7722
- detectPaths: () => [join8(appSupportDir(), "Claude")],
7723
- isInstalled: () => isInstalled([join8(appSupportDir(), "Claude")]),
7724
- globalConfigPath: () => join8(appSupportDir(), "Claude", "claude_desktop_config.json"),
7874
+ detectPaths: () => [join9(appSupportDir(), "Claude")],
7875
+ isInstalled: () => isInstalled([join9(appSupportDir(), "Claude")]),
7876
+ globalConfigPath: () => join9(appSupportDir(), "Claude", "claude_desktop_config.json"),
7725
7877
  isConfigured: () => false,
7726
7878
  install() {
7727
7879
  throw new Error("this tool only supports local (stdio) servers and cannot be configured for remote MCP");
@@ -7735,14 +7887,14 @@ var init_claude_desktop = __esm(() => {
7735
7887
  });
7736
7888
 
7737
7889
  // src/mcp/providers/cline.ts
7738
- import { join as join9 } from "node:path";
7739
- var extensionDir = () => join9(appSupportDir(), "Code", "User", "globalStorage", "saoudrizwan.claude-dev"), ClineProvider = () => createJSONProvider({
7890
+ import { join as join10 } from "node:path";
7891
+ var extensionDir = () => join10(appSupportDir(), "Code", "User", "globalStorage", "saoudrizwan.claude-dev"), ClineProvider = () => createJSONProvider({
7740
7892
  providerName: "Cline",
7741
7893
  providerID: "cline",
7742
7894
  local: false,
7743
7895
  priorityValue: 11,
7744
7896
  paths: [extensionDir()],
7745
- globalPath: join9(extensionDir(), "settings", "cline_mcp_settings.json"),
7897
+ globalPath: join10(extensionDir(), "settings", "cline_mcp_settings.json"),
7746
7898
  topKey: "mcpServers",
7747
7899
  buildServer: (cfg) => ({
7748
7900
  url: mcpURL(cfg.deployment_id),
@@ -7758,7 +7910,7 @@ var init_cline = __esm(() => {
7758
7910
  });
7759
7911
 
7760
7912
  // src/mcp/providers/cline-cli.ts
7761
- import { join as join10 } from "node:path";
7913
+ import { join as join11 } from "node:path";
7762
7914
  function clineDir() {
7763
7915
  return process.env.CLINE_DIR ?? expandHome("~/.cline");
7764
7916
  }
@@ -7768,7 +7920,7 @@ var ClineCliProvider = () => createJSONProvider({
7768
7920
  local: false,
7769
7921
  priorityValue: 12,
7770
7922
  paths: [clineDir()],
7771
- globalPath: join10(clineDir(), "data", "settings", "cline_mcp_settings.json"),
7923
+ globalPath: join11(clineDir(), "data", "settings", "cline_mcp_settings.json"),
7772
7924
  topKey: "mcpServers",
7773
7925
  buildServer: (cfg) => ({
7774
7926
  url: mcpURL(cfg.deployment_id),
@@ -7784,20 +7936,20 @@ var init_cline_cli = __esm(() => {
7784
7936
  });
7785
7937
 
7786
7938
  // src/mcp/providers/codex.ts
7787
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
7788
- import { join as join11 } from "node:path";
7939
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
7940
+ import { join as join12 } from "node:path";
7789
7941
  function codexHome() {
7790
7942
  return process.env.CODEX_HOME ?? expandHome("~/.codex");
7791
7943
  }
7792
7944
  function getConfigPath2(global) {
7793
7945
  if (global)
7794
- return join11(codexHome(), "config.toml");
7795
- return join11(process.cwd(), ".codex", "config.toml");
7946
+ return join12(codexHome(), "config.toml");
7947
+ return join12(process.cwd(), ".codex", "config.toml");
7796
7948
  }
7797
7949
  function readTOML(path) {
7798
- if (!existsSync8(path))
7950
+ if (!existsSync9(path))
7799
7951
  return "";
7800
- return readFileSync9(path, "utf-8");
7952
+ return readFileSync10(path, "utf-8");
7801
7953
  }
7802
7954
  function writeTOML(path, content) {
7803
7955
  writeSecureFile(path, content);
@@ -7855,9 +8007,9 @@ var CodexProvider = () => ({
7855
8007
  priority: () => 8,
7856
8008
  detectPaths: () => ["~/.codex"],
7857
8009
  isInstalled: () => isInstalled(["~/.codex"]),
7858
- globalConfigPath: () => join11(codexHome(), "config.toml"),
8010
+ globalConfigPath: () => join12(codexHome(), "config.toml"),
7859
8011
  isConfigured: () => {
7860
- const content = readTOML(join11(codexHome(), "config.toml"));
8012
+ const content = readTOML(join12(codexHome(), "config.toml"));
7861
8013
  return content.includes("[mcp_servers.dosu]");
7862
8014
  },
7863
8015
  install(cfg, global) {
@@ -7872,17 +8024,17 @@ var CodexProvider = () => ({
7872
8024
  writeTOML(path, removeDosuFromTOML(content));
7873
8025
  }
7874
8026
  });
7875
- var init_codex = __esm(() => {
8027
+ var init_codex2 = __esm(() => {
7876
8028
  init_config();
7877
8029
  init_config_helpers();
7878
8030
  init_detect();
7879
8031
  });
7880
8032
 
7881
8033
  // src/mcp/providers/copilot.ts
7882
- import { join as join12 } from "node:path";
8034
+ import { join as join13 } from "node:path";
7883
8035
  function globalPath() {
7884
8036
  if (process.env.XDG_CONFIG_HOME) {
7885
- return join12(process.env.XDG_CONFIG_HOME, "mcp-config.json");
8037
+ return join13(process.env.XDG_CONFIG_HOME, "mcp-config.json");
7886
8038
  }
7887
8039
  return expandHome("~/.copilot/mcp-config.json");
7888
8040
  }
@@ -7913,7 +8065,7 @@ var CopilotProvider = () => ({
7913
8065
  };
7914
8066
  installJSONServer(globalPath(), "mcpServers", server);
7915
8067
  } else {
7916
- const configPath = join12(process.cwd(), ".vscode", "mcp.json");
8068
+ const configPath = join13(process.cwd(), ".vscode", "mcp.json");
7917
8069
  const server = {
7918
8070
  type: "http",
7919
8071
  url,
@@ -7926,7 +8078,7 @@ var CopilotProvider = () => ({
7926
8078
  if (global) {
7927
8079
  removeJSONServer(globalPath(), "mcpServers");
7928
8080
  } else {
7929
- const configPath = join12(process.cwd(), ".vscode", "mcp.json");
8081
+ const configPath = join13(process.cwd(), ".vscode", "mcp.json");
7930
8082
  removeJSONServer(configPath, "servers");
7931
8083
  }
7932
8084
  }
@@ -7938,7 +8090,7 @@ var init_copilot = __esm(() => {
7938
8090
  });
7939
8091
 
7940
8092
  // src/mcp/providers/cursor.ts
7941
- import { join as join13 } from "node:path";
8093
+ import { join as join14 } from "node:path";
7942
8094
  var CursorProvider = () => createJSONProvider({
7943
8095
  providerName: "Cursor",
7944
8096
  providerID: "cursor",
@@ -7951,7 +8103,7 @@ var CursorProvider = () => createJSONProvider({
7951
8103
  url: mcpURL(cfg.deployment_id),
7952
8104
  headers: mcpHeaders(cfg.api_key)
7953
8105
  }),
7954
- localConfigPath: (cwd) => join13(cwd, ".cursor", "mcp.json")
8106
+ localConfigPath: (cwd) => join14(cwd, ".cursor", "mcp.json")
7955
8107
  });
7956
8108
  var init_cursor = __esm(() => {
7957
8109
  init_config_helpers();
@@ -7959,7 +8111,7 @@ var init_cursor = __esm(() => {
7959
8111
  });
7960
8112
 
7961
8113
  // src/mcp/providers/gemini.ts
7962
- import { join as join14 } from "node:path";
8114
+ import { join as join15 } from "node:path";
7963
8115
  var GeminiProvider = () => createJSONProvider({
7964
8116
  providerName: "Gemini CLI",
7965
8117
  providerID: "gemini",
@@ -7968,7 +8120,7 @@ var GeminiProvider = () => createJSONProvider({
7968
8120
  paths: ["~/.gemini"],
7969
8121
  globalPath: "~/.gemini/settings.json",
7970
8122
  topKey: "mcpServers",
7971
- localConfigPath: (cwd) => join14(cwd, ".gemini", "settings.json")
8123
+ localConfigPath: (cwd) => join15(cwd, ".gemini", "settings.json")
7972
8124
  });
7973
8125
  var init_gemini = __esm(() => {
7974
8126
  init_base();
@@ -8018,14 +8170,14 @@ var init_manual = __esm(() => {
8018
8170
  });
8019
8171
 
8020
8172
  // src/mcp/providers/mcporter.ts
8021
- import { existsSync as existsSync9 } from "node:fs";
8022
- import { join as join15 } from "node:path";
8173
+ import { existsSync as existsSync10 } from "node:fs";
8174
+ import { join as join16 } from "node:path";
8023
8175
  function resolveGlobalConfigPath() {
8024
8176
  const jsonPath = expandHome("~/.mcporter/mcporter.json");
8025
- if (existsSync9(jsonPath))
8177
+ if (existsSync10(jsonPath))
8026
8178
  return jsonPath;
8027
8179
  const jsoncPath = expandHome("~/.mcporter/mcporter.jsonc");
8028
- if (existsSync9(jsoncPath))
8180
+ if (existsSync10(jsoncPath))
8029
8181
  return jsoncPath;
8030
8182
  return jsonPath;
8031
8183
  }
@@ -8046,7 +8198,7 @@ var MCPorterProvider = () => ({
8046
8198
  globalConfigPath: () => resolveGlobalConfigPath(),
8047
8199
  isConfigured: () => isJSONKeyConfigured(resolveGlobalConfigPath(), "mcpServers"),
8048
8200
  install(cfg, global) {
8049
- const configPath = global ? resolveGlobalConfigPath() : join15(process.cwd(), "config", "mcporter.json");
8201
+ const configPath = global ? resolveGlobalConfigPath() : join16(process.cwd(), "config", "mcporter.json");
8050
8202
  const server = {
8051
8203
  type: "http",
8052
8204
  url: mcpEndpoint4(cfg),
@@ -8055,7 +8207,7 @@ var MCPorterProvider = () => ({
8055
8207
  installJSONServer(configPath, "mcpServers", server);
8056
8208
  },
8057
8209
  remove(global) {
8058
- const configPath = global ? resolveGlobalConfigPath() : join15(process.cwd(), "config", "mcporter.json");
8210
+ const configPath = global ? resolveGlobalConfigPath() : join16(process.cwd(), "config", "mcporter.json");
8059
8211
  removeJSONServer(configPath, "mcpServers");
8060
8212
  }
8061
8213
  });
@@ -8066,7 +8218,7 @@ var init_mcporter = __esm(() => {
8066
8218
  });
8067
8219
 
8068
8220
  // src/mcp/providers/opencode.ts
8069
- import { join as join16 } from "node:path";
8221
+ import { join as join17 } from "node:path";
8070
8222
  var OpenCodeProvider = () => createJSONProvider({
8071
8223
  providerName: "OpenCode",
8072
8224
  providerID: "opencode",
@@ -8081,7 +8233,7 @@ var OpenCodeProvider = () => createJSONProvider({
8081
8233
  enabled: true,
8082
8234
  headers: mcpHeaders(cfg.api_key)
8083
8235
  }),
8084
- localConfigPath: (cwd) => join16(cwd, "opencode.json")
8236
+ localConfigPath: (cwd) => join17(cwd, "opencode.json")
8085
8237
  });
8086
8238
  var init_opencode = __esm(() => {
8087
8239
  init_config_helpers();
@@ -8089,16 +8241,16 @@ var init_opencode = __esm(() => {
8089
8241
  });
8090
8242
 
8091
8243
  // src/mcp/providers/vscode.ts
8092
- import { join as join17 } from "node:path";
8244
+ import { join as join18 } from "node:path";
8093
8245
  var VSCodeProvider = () => createJSONProvider({
8094
8246
  providerName: "VS Code",
8095
8247
  providerID: "vscode",
8096
8248
  local: true,
8097
8249
  priorityValue: 6,
8098
- paths: [join17(appSupportDir(), "Code")],
8099
- globalPath: join17(appSupportDir(), "Code", "User", "mcp.json"),
8250
+ paths: [join18(appSupportDir(), "Code")],
8251
+ globalPath: join18(appSupportDir(), "Code", "User", "mcp.json"),
8100
8252
  topKey: "servers",
8101
- localConfigPath: (cwd) => join17(cwd, ".vscode", "mcp.json")
8253
+ localConfigPath: (cwd) => join18(cwd, ".vscode", "mcp.json")
8102
8254
  });
8103
8255
  var init_vscode = __esm(() => {
8104
8256
  init_detect();
@@ -8107,14 +8259,14 @@ var init_vscode = __esm(() => {
8107
8259
 
8108
8260
  // src/mcp/providers/windsurf.ts
8109
8261
  import { homedir as homedir2 } from "node:os";
8110
- import { join as join18 } from "node:path";
8262
+ import { join as join19 } from "node:path";
8111
8263
  var WindsurfProvider = () => createJSONProvider({
8112
8264
  providerName: "Windsurf",
8113
8265
  providerID: "windsurf",
8114
8266
  local: false,
8115
8267
  priorityValue: 9,
8116
- paths: [join18(homedir2(), ".codeium", "windsurf")],
8117
- globalPath: join18(homedir2(), ".codeium", "windsurf", "mcp_config.json"),
8268
+ paths: [join19(homedir2(), ".codeium", "windsurf")],
8269
+ globalPath: join19(homedir2(), ".codeium", "windsurf", "mcp_config.json"),
8118
8270
  topKey: "mcpServers"
8119
8271
  });
8120
8272
  var init_windsurf = __esm(() => {
@@ -8123,12 +8275,12 @@ var init_windsurf = __esm(() => {
8123
8275
 
8124
8276
  // src/mcp/providers/zed.ts
8125
8277
  import { platform as platform2 } from "node:os";
8126
- import { join as join19 } from "node:path";
8278
+ import { join as join20 } from "node:path";
8127
8279
  function zedConfigDir() {
8128
8280
  const os = platform2();
8129
8281
  if (os === "darwin" || os === "win32")
8130
- return join19(appSupportDir(), "Zed");
8131
- return join19(appSupportDir(), "zed");
8282
+ return join20(appSupportDir(), "Zed");
8283
+ return join20(appSupportDir(), "zed");
8132
8284
  }
8133
8285
  var ZedProvider = () => createJSONProvider({
8134
8286
  providerName: "Zed",
@@ -8136,7 +8288,7 @@ var ZedProvider = () => createJSONProvider({
8136
8288
  local: true,
8137
8289
  priorityValue: 10,
8138
8290
  paths: [zedConfigDir()],
8139
- globalPath: join19(zedConfigDir(), "settings.json"),
8291
+ globalPath: join20(zedConfigDir(), "settings.json"),
8140
8292
  topKey: "context_servers",
8141
8293
  buildServer: (cfg) => ({
8142
8294
  source: "custom",
@@ -8144,7 +8296,7 @@ var ZedProvider = () => createJSONProvider({
8144
8296
  url: mcpURL(cfg.deployment_id),
8145
8297
  headers: mcpHeaders(cfg.api_key)
8146
8298
  }),
8147
- localConfigPath: (cwd) => join19(cwd, ".zed", "settings.json")
8299
+ localConfigPath: (cwd) => join20(cwd, ".zed", "settings.json")
8148
8300
  });
8149
8301
  var init_zed = __esm(() => {
8150
8302
  init_config_helpers();
@@ -8188,7 +8340,7 @@ var init_providers = __esm(() => {
8188
8340
  init_claude_desktop();
8189
8341
  init_cline();
8190
8342
  init_cline_cli();
8191
- init_codex();
8343
+ init_codex2();
8192
8344
  init_copilot();
8193
8345
  init_cursor();
8194
8346
  init_gemini();
@@ -10737,8 +10889,8 @@ __export(exports_flow2, {
10737
10889
  isStdioOnly: () => isStdioOnly
10738
10890
  });
10739
10891
  import { randomUUID } from "node:crypto";
10740
- import { existsSync as existsSync10 } from "node:fs";
10741
- import { join as join20 } from "node:path";
10892
+ import { existsSync as existsSync11 } from "node:fs";
10893
+ import { join as join21 } from "node:path";
10742
10894
  async function runSetup(opts = {}) {
10743
10895
  const onboardingRunID = randomUUID();
10744
10896
  logger.info("setup", `Setup flow started${opts.deploymentID ? ` deployment=${opts.deploymentID}` : ""}${opts.mode ? ` mode=${opts.mode}` : ""}`);
@@ -10910,7 +11062,7 @@ async function runSetup(opts = {}) {
10910
11062
  showTryItOutPrompt({
10911
11063
  mode: cfg.mode,
10912
11064
  docsImported: choices.connectGitHub && githubOnboardingDone,
10913
- hasAgentsMd: existsSync10(join20(process.cwd(), "AGENTS.md"))
11065
+ hasAgentsMd: existsSync11(join21(process.cwd(), "AGENTS.md"))
10914
11066
  });
10915
11067
  }
10916
11068
  if (mcpCompleted || skillCompleted || docsImported) {
@@ -11404,8 +11556,8 @@ var init_flow2 = __esm(() => {
11404
11556
  });
11405
11557
 
11406
11558
  // src/commands/insights.ts
11407
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
11408
- import { dirname as dirname3, join as join21 } from "node:path";
11559
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
11560
+ import { dirname as dirname3, join as join22 } from "node:path";
11409
11561
  function shuffled(arr) {
11410
11562
  const copy2 = [...arr];
11411
11563
  for (let i = copy2.length - 1;i > 0; i--) {
@@ -11509,17 +11661,17 @@ function makeAskFn(cfg) {
11509
11661
  };
11510
11662
  }
11511
11663
  function insightsDir() {
11512
- return join21(getConfigDir(), "insights");
11664
+ return join22(getConfigDir(), "insights");
11513
11665
  }
11514
11666
  function reportPath(timestamp) {
11515
11667
  const dir = insightsDir();
11516
11668
  if (!timestamp)
11517
- return join21(dir, "latest.html");
11669
+ return join22(dir, "latest.html");
11518
11670
  const iso = timestamp.toISOString().slice(0, 19).replace(/:/g, "-");
11519
- return join21(dir, `report-${iso}Z.html`);
11671
+ return join22(dir, `report-${iso}Z.html`);
11520
11672
  }
11521
11673
  function pruneOldReports(dir, keepN) {
11522
- if (!existsSync11(dir))
11674
+ if (!existsSync12(dir))
11523
11675
  return;
11524
11676
  let entries;
11525
11677
  try {
@@ -11531,7 +11683,7 @@ function pruneOldReports(dir, keepN) {
11531
11683
  const reports = entries.filter((f) => REPORT_FILE_PATTERN.test(f)).sort().reverse();
11532
11684
  for (const f of reports.slice(keepN)) {
11533
11685
  try {
11534
- unlinkSync(join21(dir, f));
11686
+ unlinkSync(join22(dir, f));
11535
11687
  } catch (err) {
11536
11688
  logger.debug("insights", `failed to prune ${f}: ${err instanceof Error ? err.message : err}`);
11537
11689
  }
@@ -11604,7 +11756,7 @@ function defaultRunner(cfg) {
11604
11756
  render: renderHTML,
11605
11757
  writeFile: (path2, content) => {
11606
11758
  const dir = dirname3(path2);
11607
- if (!existsSync11(dir))
11759
+ if (!existsSync12(dir))
11608
11760
  mkdirSync6(dir, { recursive: true, mode: 448 });
11609
11761
  writeFileSync5(path2, content, { mode: 384 });
11610
11762
  },
@@ -12269,7 +12421,7 @@ var init_flow3 = __esm(() => {
12269
12421
  // src/cli/cli.ts
12270
12422
  init_esm();
12271
12423
  init_client();
12272
- import { readFileSync as readFileSync11, unlinkSync as unlinkSync2 } from "node:fs";
12424
+ import { readFileSync as readFileSync12, unlinkSync as unlinkSync2 } from "node:fs";
12273
12425
 
12274
12426
  // src/commands/analytics.ts
12275
12427
  init_esm();
@@ -12954,7 +13106,8 @@ ${page.body}`);
12954
13106
  init_esm();
12955
13107
  init_config();
12956
13108
  init_logger();
12957
- import { readFileSync as readFileSync7 } from "node:fs";
13109
+ init_prompts();
13110
+ import { readFileSync as readFileSync8 } from "node:fs";
12958
13111
  import { basename } from "node:path";
12959
13112
 
12960
13113
  // src/hooks/state.ts
@@ -12993,7 +13146,8 @@ var COOLDOWN_DEFAULT_MS = 3000;
12993
13146
  var TTL_DEFAULT_MS = 10 * 60 * 1000;
12994
13147
  var STOP_WAIT_DEFAULT_MS = 8000;
12995
13148
  var STOP_POLL_DEFAULT_MS = 1000;
12996
- var SUPPORTED_AGENTS = ["claude-code"];
13149
+ var SUPPORTED_AGENTS = ["claude-code", "codex"];
13150
+ var DEFAULT_AGENT = "claude-code";
12997
13151
  function cooldownMs() {
12998
13152
  const n = Number.parseInt(process.env.DOSU_HOOK_CHECK_COOLDOWN_MS ?? "", 10);
12999
13153
  return Number.isFinite(n) && n >= 0 ? n : COOLDOWN_DEFAULT_MS;
@@ -13026,13 +13180,17 @@ function errMsg(err) {
13026
13180
  function repoSlug(cwd) {
13027
13181
  return cwd ? basename(cwd) : undefined;
13028
13182
  }
13029
- async function runUserPromptSubmit(input, now = Date.now()) {
13183
+ async function runUserPromptSubmit(input, now = Date.now(), agent = DEFAULT_AGENT) {
13030
13184
  const sessionId = input.session_id;
13031
13185
  if (!sessionId)
13032
13186
  return;
13033
13187
  const prompt = (input.prompt ?? "").trim();
13034
13188
  if (!prompt)
13035
13189
  return;
13190
+ if (prompt.startsWith(STOP_PREFIX)) {
13191
+ logger.debug("hooks", "submit skipped reason=own-stop-envelope");
13192
+ return;
13193
+ }
13036
13194
  const existing = loadState(sessionId);
13037
13195
  if (existing && existing.status === "pending" && now <= existing.expiresAt) {
13038
13196
  logger.debug("hooks", `submit sid=${sid8(sessionId)} reuse tid=${existing.ticketId}`);
@@ -13049,7 +13207,7 @@ async function runUserPromptSubmit(input, now = Date.now()) {
13049
13207
  try {
13050
13208
  resp = await tc.requestCreateTicket(cfg, {
13051
13209
  deployment_id: cfg.deployment_id,
13052
- agent: "claude-code",
13210
+ agent,
13053
13211
  session_id: sessionId,
13054
13212
  turn_id: input.turn_id ?? String(now),
13055
13213
  prompt,
@@ -13196,7 +13354,7 @@ function runStatus(input, opts, now = Date.now()) {
13196
13354
  console.log(`Status: ${data.status}`);
13197
13355
  console.log(`Delivered: ${data.delivered ? "yes" : "no"}`);
13198
13356
  }
13199
- async function runHookEntrypoint(event, raw, now = Date.now()) {
13357
+ async function runHookEntrypoint(event, raw, now = Date.now(), agent = DEFAULT_AGENT) {
13200
13358
  let input = {};
13201
13359
  try {
13202
13360
  input = raw.trim() ? JSON.parse(raw) : {};
@@ -13205,7 +13363,7 @@ async function runHookEntrypoint(event, raw, now = Date.now()) {
13205
13363
  }
13206
13364
  try {
13207
13365
  if (event === "user-prompt-submit")
13208
- await runUserPromptSubmit(input, now);
13366
+ await runUserPromptSubmit(input, now, agent);
13209
13367
  else if (event === "post-tool-use")
13210
13368
  await runPostToolUse(input, now);
13211
13369
  else
@@ -13218,7 +13376,7 @@ async function runHookEntrypoint(event, raw, now = Date.now()) {
13218
13376
  }
13219
13377
  function readStdinRaw() {
13220
13378
  try {
13221
- return readFileSync7(0, "utf8");
13379
+ return readFileSync8(0, "utf8");
13222
13380
  } catch {
13223
13381
  return "";
13224
13382
  }
@@ -13231,12 +13389,13 @@ async function emitErrorLine(step, reason, agentNextSteps) {
13231
13389
  emitError2({ step, reason, agent_next_steps: agentNextSteps });
13232
13390
  }
13233
13391
  async function runInstall(agent, opts) {
13234
- if (agent !== "claude-code") {
13392
+ if (!SUPPORTED_AGENTS.includes(agent)) {
13235
13393
  process.exitCode = 2;
13394
+ const supported = SUPPORTED_AGENTS.join(", ");
13236
13395
  if (opts.json) {
13237
- await emitErrorLine("hooks-install", "unsupported_agent", `Only 'claude-code' is supported.`);
13396
+ await emitErrorLine("hooks-install", "unsupported_agent", `Supported agents: ${supported}.`);
13238
13397
  } else {
13239
- console.error(`Unsupported agent '${agent}'. Supported agents: claude-code.`);
13398
+ console.error(`Unsupported agent '${agent}'. Supported agents: ${supported}.`);
13240
13399
  }
13241
13400
  return;
13242
13401
  }
@@ -13249,13 +13408,28 @@ async function runInstall(agent, opts) {
13249
13408
  }
13250
13409
  return;
13251
13410
  }
13252
- const { installClaudeHooks: installClaudeHooks2, claudeLocalSettingsPath: claudeLocalSettingsPath2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13253
- const configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13254
13411
  try {
13412
+ if (agent === "codex") {
13413
+ const { installCodexHooks: installCodexHooks2, codexHooksPath: codexHooksPath2 } = await Promise.resolve().then(() => (init_codex(), exports_codex));
13414
+ const configPath2 = codexHooksPath2(resolveDir(opts.dir));
13415
+ const { events: events2 } = installCodexHooks2(configPath2, { stop: opts.stop });
13416
+ if (opts.json) {
13417
+ const { emitStep: emitStep2 } = await Promise.resolve().then(() => exports_output);
13418
+ emitStep2({ step: "hooks-install", agent, path: configPath2, events: events2 });
13419
+ } else {
13420
+ console.log(`✓ Installed Dosu hooks for Codex (${events2.join(", ")}).`);
13421
+ console.log(` → ${configPath2}`);
13422
+ console.log(" One-time step: open Codex in this project and run /hooks to review and");
13423
+ console.log(" trust the Dosu hooks — Codex skips untrusted hooks by design.");
13424
+ }
13425
+ return;
13426
+ }
13427
+ const { installClaudeHooks: installClaudeHooks2, claudeLocalSettingsPath: claudeLocalSettingsPath2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13428
+ const configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13255
13429
  const { events } = installClaudeHooks2(configPath, { stop: opts.stop });
13256
13430
  if (opts.json) {
13257
13431
  const { emitStep: emitStep2 } = await Promise.resolve().then(() => exports_output);
13258
- emitStep2({ step: "hooks-install", path: configPath, events });
13432
+ emitStep2({ step: "hooks-install", agent, path: configPath, events });
13259
13433
  } else {
13260
13434
  console.log(`✓ Installed Dosu hooks for Claude Code (${events.join(", ")}).`);
13261
13435
  console.log(` → ${configPath}`);
@@ -13271,18 +13445,27 @@ async function runInstall(agent, opts) {
13271
13445
  }
13272
13446
  }
13273
13447
  async function runUninstall(agent, opts) {
13274
- if (agent !== "claude-code") {
13448
+ if (!SUPPORTED_AGENTS.includes(agent)) {
13275
13449
  process.exitCode = 2;
13450
+ const supported = SUPPORTED_AGENTS.join(", ");
13276
13451
  if (opts.json) {
13277
- await emitErrorLine("hooks-uninstall", "unsupported_agent", "Only 'claude-code' is supported.");
13452
+ await emitErrorLine("hooks-uninstall", "unsupported_agent", `Supported agents: ${supported}.`);
13278
13453
  } else {
13279
- console.error(`Unsupported agent '${agent}'. Supported agents: claude-code.`);
13454
+ console.error(`Unsupported agent '${agent}'. Supported agents: ${supported}.`);
13280
13455
  }
13281
13456
  return;
13282
13457
  }
13283
- const { removeClaudeHooks: removeClaudeHooks2, claudeLocalSettingsPath: claudeLocalSettingsPath2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13284
- const configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13285
- const { removed } = removeClaudeHooks2(configPath);
13458
+ let configPath;
13459
+ let removed;
13460
+ if (agent === "codex") {
13461
+ const { removeCodexHooks: removeCodexHooks2, codexHooksPath: codexHooksPath2 } = await Promise.resolve().then(() => (init_codex(), exports_codex));
13462
+ configPath = codexHooksPath2(resolveDir(opts.dir));
13463
+ ({ removed } = removeCodexHooks2(configPath));
13464
+ } else {
13465
+ const { removeClaudeHooks: removeClaudeHooks2, claudeLocalSettingsPath: claudeLocalSettingsPath2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13466
+ configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13467
+ ({ removed } = removeClaudeHooks2(configPath));
13468
+ }
13286
13469
  if (opts.json) {
13287
13470
  const { emitStep: emitStep2 } = await Promise.resolve().then(() => exports_output);
13288
13471
  emitStep2({ step: "hooks-uninstall", path: configPath, removed });
@@ -13295,12 +13478,16 @@ async function runUninstall(agent, opts) {
13295
13478
  async function collectDoctorChecks(opts) {
13296
13479
  const checks = [];
13297
13480
  const { claudeLocalSettingsPath: claudeLocalSettingsPath2, inspectClaudeHooks: inspectClaudeHooks2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13481
+ const { codexHooksPath: codexHooksPath2, inspectCodexHooks: inspectCodexHooks2 } = await Promise.resolve().then(() => (init_codex(), exports_codex));
13298
13482
  const configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13299
13483
  const inspection = inspectClaudeHooks2(configPath);
13484
+ const codexPath = codexHooksPath2(resolveDir(opts.dir));
13485
+ const codex = inspectCodexHooks2(codexPath);
13486
+ const codexInstalled = codex.events.includes("UserPromptSubmit") && codex.events.includes("PostToolUse");
13300
13487
  if (!inspection.fileExists) {
13301
13488
  checks.push({
13302
13489
  name: "config",
13303
- status: "fail",
13490
+ status: codexInstalled ? "warn" : "fail",
13304
13491
  detail: `not found: ${configPath} (run 'dosu hooks install claude-code')`
13305
13492
  });
13306
13493
  } else if (inspection.parseError) {
@@ -13319,10 +13506,32 @@ async function collectDoctorChecks(opts) {
13319
13506
  } else {
13320
13507
  checks.push({
13321
13508
  name: "hooks",
13322
- status: "fail",
13509
+ status: codexInstalled ? "warn" : "fail",
13323
13510
  detail: "UserPromptSubmit + PostToolUse not both installed"
13324
13511
  });
13325
13512
  }
13513
+ if (codex.fileExists) {
13514
+ if (codex.parseError) {
13515
+ checks.push({ name: "codex-config", status: "fail", detail: `invalid JSON: ${codexPath}` });
13516
+ } else if (codexInstalled) {
13517
+ checks.push({
13518
+ name: "codex-config",
13519
+ status: "ok",
13520
+ detail: `installed: ${codex.events.join(", ")} (${codexPath})`
13521
+ });
13522
+ checks.push({
13523
+ name: "codex-trust",
13524
+ status: "warn",
13525
+ detail: "if hooks aren't firing, run /hooks inside Codex to review + trust them (one-time)"
13526
+ });
13527
+ } else {
13528
+ checks.push({
13529
+ name: "codex-config",
13530
+ status: "fail",
13531
+ detail: "UserPromptSubmit + PostToolUse not both installed"
13532
+ });
13533
+ }
13534
+ }
13326
13535
  const cfg = loadConfig();
13327
13536
  if (isAuthenticated(cfg)) {
13328
13537
  checks.push({ name: "auth", status: "ok", detail: "logged in" });
@@ -13377,15 +13586,15 @@ async function runDoctor(opts) {
13377
13586
  }
13378
13587
  }
13379
13588
  function hooksCommand() {
13380
- const cmd = new Command("hooks").description("Dosu knowledge-injection hooks for Claude Code");
13381
- cmd.command("user-prompt-submit").description("Hook entrypoint: create a knowledge ticket on prompt submit").action(async () => {
13382
- await runHookEntrypoint("user-prompt-submit", readStdinRaw());
13589
+ const cmd = new Command("hooks").description("Dosu knowledge-injection hooks for coding agents (Claude Code, Codex)");
13590
+ cmd.command("user-prompt-submit").description("Hook entrypoint: create a knowledge ticket on prompt submit").option("--agent <agent>", "Calling agent for ticket attribution", DEFAULT_AGENT).action(async (opts) => {
13591
+ await runHookEntrypoint("user-prompt-submit", readStdinRaw(), Date.now(), opts.agent);
13383
13592
  });
13384
- cmd.command("post-tool-use").description("Hook entrypoint: poll and inject ready knowledge once").action(async () => {
13385
- await runHookEntrypoint("post-tool-use", readStdinRaw());
13593
+ cmd.command("post-tool-use").description("Hook entrypoint: poll and inject ready knowledge once").option("--agent <agent>", "Calling agent for ticket attribution", DEFAULT_AGENT).action(async (opts) => {
13594
+ await runHookEntrypoint("post-tool-use", readStdinRaw(), Date.now(), opts.agent);
13386
13595
  });
13387
- cmd.command("stop").description("Hook entrypoint: last-chance knowledge delivery when the agent stops").action(async () => {
13388
- await runHookEntrypoint("stop", readStdinRaw());
13596
+ cmd.command("stop").description("Hook entrypoint: last-chance knowledge delivery when the agent stops").option("--agent <agent>", "Calling agent for ticket attribution", DEFAULT_AGENT).action(async (opts) => {
13597
+ await runHookEntrypoint("stop", readStdinRaw(), Date.now(), opts.agent);
13389
13598
  });
13390
13599
  cmd.command("status").description("Show the active Dosu knowledge ticket for this session").option("--json", "Output JSON", false).action((opts) => {
13391
13600
  let input = {};
@@ -13403,16 +13612,21 @@ function hooksCommand() {
13403
13612
  "",
13404
13613
  "Examples:",
13405
13614
  " $ dosu hooks install claude-code",
13615
+ " $ dosu hooks install codex",
13406
13616
  " $ dosu hooks install claude-code --no-stop",
13407
- " $ dosu hooks install claude-code --dir ./my-project"
13617
+ " $ dosu hooks install codex --dir ./my-project",
13618
+ "",
13619
+ "Codex requires a one-time trust step after install: open Codex in the",
13620
+ "project and run /hooks to review and trust the Dosu hooks."
13408
13621
  ].join(`
13409
13622
  `)).action((agent, opts) => runInstall(agent, opts));
13410
13623
  cmd.command("uninstall").description("Remove Dosu hooks from a coding agent's local config (Dosu-owned entries only)").addArgument(new Argument("<agent>", "coding agent to clean up").choices(SUPPORTED_AGENTS)).option("--scope <scope>", "Config scope (local only)", "local").option("--dir <path>", "Project root (defaults to current directory)").option("--json", "Emit machine-readable JSON", false).addHelpText("after", [
13411
13624
  "",
13412
13625
  `Supported agents: ${SUPPORTED_AGENTS.join(", ")}`,
13413
13626
  "",
13414
- "Example:",
13415
- " $ dosu hooks uninstall claude-code"
13627
+ "Examples:",
13628
+ " $ dosu hooks uninstall claude-code",
13629
+ " $ dosu hooks uninstall codex"
13416
13630
  ].join(`
13417
13631
  `)).action((agent, opts) => runUninstall(agent, opts));
13418
13632
  cmd.command("doctor").description("Diagnose Dosu hook config, auth, deployment, and backend connectivity").option("--dir <path>", "Project root (defaults to current directory)").option("--json", "Emit machine-readable JSON", false).action((opts) => runDoctor(opts));
@@ -14248,8 +14462,8 @@ init_skill_update_check();
14248
14462
  init_config();
14249
14463
  init_logger();
14250
14464
  var import_picocolors24 = __toESM(require_picocolors(), 1);
14251
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
14252
- import { join as join22 } from "node:path";
14465
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
14466
+ import { join as join23 } from "node:path";
14253
14467
  var CACHE_FILENAME2 = "update-check.json";
14254
14468
  var CHECK_INTERVAL_MS2 = 24 * 60 * 60 * 1000;
14255
14469
  var FETCH_TIMEOUT_MS2 = 5000;
@@ -14272,14 +14486,14 @@ function isNewerVersion(latest, current) {
14272
14486
  return false;
14273
14487
  }
14274
14488
  function getCachePath2() {
14275
- return join22(getConfigDir(), CACHE_FILENAME2);
14489
+ return join23(getConfigDir(), CACHE_FILENAME2);
14276
14490
  }
14277
14491
  function readCache() {
14278
14492
  try {
14279
14493
  const path2 = getCachePath2();
14280
- if (!existsSync12(path2))
14494
+ if (!existsSync13(path2))
14281
14495
  return null;
14282
- const data = JSON.parse(readFileSync10(path2, "utf-8"));
14496
+ const data = JSON.parse(readFileSync11(path2, "utf-8"));
14283
14497
  if (typeof data.lastCheck === "number" && typeof data.latestVersion === "string") {
14284
14498
  return data;
14285
14499
  }
@@ -14291,7 +14505,7 @@ function readCache() {
14291
14505
  function writeCache(cache) {
14292
14506
  try {
14293
14507
  const dir = getConfigDir();
14294
- if (!existsSync12(dir)) {
14508
+ if (!existsSync13(dir)) {
14295
14509
  mkdirSync7(dir, { recursive: true, mode: 448 });
14296
14510
  }
14297
14511
  writeFileSync6(getCachePath2(), JSON.stringify(cache), { mode: 384 });
@@ -14581,7 +14795,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
14581
14795
  if (opts.tail !== undefined) {
14582
14796
  const n = typeof opts.tail === "string" ? parseInt(opts.tail, 10) || 50 : 50;
14583
14797
  try {
14584
- const content = readFileSync11(logPath, "utf-8");
14798
+ const content = readFileSync12(logPath, "utf-8");
14585
14799
  const lines = content.split(`
14586
14800
  `);
14587
14801
  console.log(lines.slice(-n).join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Manage MCP servers for AI tools",
6
6
  "license": "MIT",