@dosu/cli 0.16.1 → 0.17.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 +85 -43
  2. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -2218,6 +2218,34 @@ class Client {
2218
2218
  async delete(path) {
2219
2219
  return this.doRequest("DELETE", path);
2220
2220
  }
2221
+ async getWithApiKey(path) {
2222
+ return this.apiKeyRequest("GET", path);
2223
+ }
2224
+ async postWithApiKey(path, body) {
2225
+ return this.apiKeyRequest("POST", path, body);
2226
+ }
2227
+ async apiKeyRequest(method, path, body) {
2228
+ if (!this.config.api_key) {
2229
+ throw new Error("no API key available");
2230
+ }
2231
+ const url = this.baseURL + path;
2232
+ const headers = {
2233
+ "Content-Type": "application/json",
2234
+ "X-Dosu-API-Key": this.config.api_key
2235
+ };
2236
+ const options = { method, headers };
2237
+ if (body !== undefined) {
2238
+ options.body = JSON.stringify(body);
2239
+ }
2240
+ const controller = new AbortController;
2241
+ const timeout = setTimeout(() => controller.abort(), 1e4);
2242
+ options.signal = controller.signal;
2243
+ try {
2244
+ return await fetch(url, options);
2245
+ } finally {
2246
+ clearTimeout(timeout);
2247
+ }
2248
+ }
2221
2249
  async refreshToken() {
2222
2250
  if (!this.config.refresh_token) {
2223
2251
  throw new Error("no refresh token available");
@@ -4497,7 +4525,7 @@ var init_dist4 = __esm(() => {
4497
4525
  function getVersionString() {
4498
4526
  return `v${VERSION}`;
4499
4527
  }
4500
- var VERSION = "0.16.1";
4528
+ var VERSION = "0.17.0";
4501
4529
 
4502
4530
  // src/debug/logger.ts
4503
4531
  import {
@@ -5408,7 +5436,7 @@ function isDefinitiveError(err) {
5408
5436
  }
5409
5437
  async function requestCreateTicket(cfg, req) {
5410
5438
  const client = new Client(cfg);
5411
- const resp = await client.post(TICKETS_PATH, req);
5439
+ const resp = await client.postWithApiKey(TICKETS_PATH, req);
5412
5440
  if (resp.status !== 202 && resp.status !== 200 && resp.status !== 201) {
5413
5441
  throw new TicketHttpError(resp.status);
5414
5442
  }
@@ -5416,7 +5444,7 @@ async function requestCreateTicket(cfg, req) {
5416
5444
  }
5417
5445
  async function requestGetTicket(cfg, ticketId) {
5418
5446
  const client = new Client(cfg);
5419
- const resp = await client.get(`${TICKETS_PATH}/${encodeURIComponent(ticketId)}`);
5447
+ const resp = await client.getWithApiKey(`${TICKETS_PATH}/${encodeURIComponent(ticketId)}`);
5420
5448
  if (resp.status !== 200 && resp.status !== 202) {
5421
5449
  throw new TicketHttpError(resp.status);
5422
5450
  }
@@ -6224,7 +6252,8 @@ function installClaudeHooks(configPath, opts = {}) {
6224
6252
  if (typeof cfg.hooks !== "object" || cfg.hooks === null) {
6225
6253
  cfg.hooks = {};
6226
6254
  }
6227
- const events = [...DEFAULT_HOOK_EVENTS, ...opts.withStop ? ["Stop"] : []];
6255
+ const includeStop = opts.stop !== false;
6256
+ const events = DEFAULT_HOOK_EVENTS.filter((event) => event !== "Stop" || includeStop);
6228
6257
  for (const event of events) {
6229
6258
  const existing = Array.isArray(cfg.hooks[event]) ? cfg.hooks[event] : [];
6230
6259
  const preserved = existing.filter((g2) => !isDosuGroup(g2));
@@ -6284,7 +6313,7 @@ function inspectClaudeHooks(configPath) {
6284
6313
  var DEFAULT_HOOK_EVENTS, MARKER = "__dosu", EVENT_SUBCOMMAND;
6285
6314
  var init_claude_code = __esm(() => {
6286
6315
  init_config_helpers();
6287
- DEFAULT_HOOK_EVENTS = ["UserPromptSubmit", "PostToolUse"];
6316
+ DEFAULT_HOOK_EVENTS = ["UserPromptSubmit", "PostToolUse", "Stop"];
6288
6317
  EVENT_SUBCOMMAND = {
6289
6318
  UserPromptSubmit: "user-prompt-submit",
6290
6319
  PostToolUse: "post-tool-use",
@@ -9389,7 +9418,7 @@ function hasNewVisibleRepository(previousRepos, nextRepos) {
9389
9418
  const previousRepoIds = new Set(previousRepos.map((repo) => repo.repository_id));
9390
9419
  return nextRepos.some((repo) => !previousRepoIds.has(repo.repository_id));
9391
9420
  }
9392
- function sleep2(ms) {
9421
+ function sleep3(ms) {
9393
9422
  return new Promise((resolve) => setTimeout(resolve, ms));
9394
9423
  }
9395
9424
  async function waitForRepositoryRefresh(trpc, orgID, previousRepos, opts) {
@@ -9403,7 +9432,7 @@ async function waitForRepositoryRefresh(trpc, orgID, previousRepos, opts) {
9403
9432
  if (hasNewVisibleRepository(previousRepos, latestRepos)) {
9404
9433
  return { repos: latestRepos, foundNew: true };
9405
9434
  }
9406
- await sleep2(intervalMs);
9435
+ await sleep3(intervalMs);
9407
9436
  }
9408
9437
  return { repos: latestRepos, foundNew: false };
9409
9438
  }
@@ -9536,7 +9565,7 @@ async function verifyDataSourcesPersist(trpc, orgID, expectedDataSourceIds, opts
9536
9565
  return { alive, dropped };
9537
9566
  if (timeoutMs === 0)
9538
9567
  break;
9539
- await sleep2(intervalMs);
9568
+ await sleep3(intervalMs);
9540
9569
  }
9541
9570
  return { alive, dropped };
9542
9571
  }
@@ -10136,7 +10165,7 @@ async function waitForImportTaskCompletion(trpc, taskID, spinner, expectedTotal)
10136
10165
  return null;
10137
10166
  }
10138
10167
  }
10139
- await sleep3(IMPORT_STATUS_POLL_INTERVAL_MS);
10168
+ await sleep4(IMPORT_STATUS_POLL_INTERVAL_MS);
10140
10169
  }
10141
10170
  }
10142
10171
  function buildImportProgressMessage(status, expectedTotal) {
@@ -10187,7 +10216,7 @@ async function waitForImportableGithubFiles(trpc, orgID, spaceID, expectedDataSo
10187
10216
  spinner.stop(latestFiles.length > 0 ? "Docs ready" : "No markdown docs found");
10188
10217
  return latestFiles;
10189
10218
  }
10190
- await sleep3(DOC_SCAN_POLL_INTERVAL_MS);
10219
+ await sleep4(DOC_SCAN_POLL_INTERVAL_MS);
10191
10220
  continue;
10192
10221
  }
10193
10222
  latestFiles = await fetchImportableGithubFiles(trpc, spaceID);
@@ -10199,7 +10228,7 @@ async function waitForImportableGithubFiles(trpc, orgID, spaceID, expectedDataSo
10199
10228
  spinner.stop("No markdown docs found");
10200
10229
  return [];
10201
10230
  }
10202
- await sleep3(DOC_SCAN_POLL_INTERVAL_MS);
10231
+ await sleep4(DOC_SCAN_POLL_INTERVAL_MS);
10203
10232
  }
10204
10233
  spinner.stop("Timed out");
10205
10234
  const choice = await ve({
@@ -10293,7 +10322,7 @@ async function getKnowledgeStoreID(trpc, spaceID) {
10293
10322
  return null;
10294
10323
  }
10295
10324
  }
10296
- function sleep3(ms) {
10325
+ function sleep4(ms) {
10297
10326
  return new Promise((resolve) => setTimeout(resolve, ms));
10298
10327
  }
10299
10328
  var DOC_SCAN_POLL_INTERVAL_MS = 2000, DOC_SCAN_POLL_TIMEOUT_MS = 60000, IMPORT_STATUS_POLL_INTERVAL_MS = 2000, IMPORT_STATUS_MAX_ERRORS = 3;
@@ -12962,6 +12991,8 @@ function saveState(state) {
12962
12991
  // src/commands/hooks.ts
12963
12992
  var COOLDOWN_DEFAULT_MS = 3000;
12964
12993
  var TTL_DEFAULT_MS = 10 * 60 * 1000;
12994
+ var STOP_WAIT_DEFAULT_MS = 8000;
12995
+ var STOP_POLL_DEFAULT_MS = 1000;
12965
12996
  var SUPPORTED_AGENTS = ["claude-code"];
12966
12997
  function cooldownMs() {
12967
12998
  const n = Number.parseInt(process.env.DOSU_HOOK_CHECK_COOLDOWN_MS ?? "", 10);
@@ -12971,12 +13002,21 @@ function ttlMs() {
12971
13002
  const n = Number.parseInt(process.env.DOSU_HOOK_TTL_MS ?? "", 10);
12972
13003
  return Number.isFinite(n) && n > 0 ? n : TTL_DEFAULT_MS;
12973
13004
  }
13005
+ function stopWaitMs() {
13006
+ const n = Number.parseInt(process.env.DOSU_HOOK_STOP_WAIT_MS ?? "", 10);
13007
+ return Number.isFinite(n) && n >= 0 ? n : STOP_WAIT_DEFAULT_MS;
13008
+ }
13009
+ function stopPollMs() {
13010
+ const n = Number.parseInt(process.env.DOSU_HOOK_STOP_POLL_MS ?? "", 10);
13011
+ return Number.isFinite(n) && n > 0 ? n : STOP_POLL_DEFAULT_MS;
13012
+ }
12974
13013
  function printHookContext(event, additionalContext) {
12975
13014
  console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext } }));
12976
13015
  }
12977
13016
  function printContinue() {
12978
13017
  console.log(JSON.stringify({ continue: true }));
12979
13018
  }
13019
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12980
13020
  function sid8(sessionId) {
12981
13021
  return sessionId.slice(0, 8);
12982
13022
  }
@@ -12999,7 +13039,7 @@ async function runUserPromptSubmit(input, now = Date.now()) {
12999
13039
  return;
13000
13040
  }
13001
13041
  const cfg = loadConfig();
13002
- if (!isAuthenticated(cfg) || !cfg.deployment_id) {
13042
+ if (!cfg.api_key || !cfg.deployment_id) {
13003
13043
  logger.debug("hooks", "submit skipped reason=not-configured");
13004
13044
  return;
13005
13045
  }
@@ -13053,7 +13093,7 @@ async function runPostToolUse(input, now = Date.now()) {
13053
13093
  return;
13054
13094
  }
13055
13095
  const cfg = loadConfig();
13056
- if (!isAuthenticated(cfg) || !cfg.deployment_id) {
13096
+ if (!cfg.api_key || !cfg.deployment_id) {
13057
13097
  saveState({ ...state, lastCheckedAt: now });
13058
13098
  return;
13059
13099
  }
@@ -13099,40 +13139,42 @@ async function runStop(input, now = Date.now()) {
13099
13139
  const sessionId = input.session_id;
13100
13140
  if (!sessionId)
13101
13141
  return printContinue();
13102
- if (input.stop_hook_active === true)
13103
- return printContinue();
13104
13142
  const state = loadState(sessionId);
13105
13143
  if (!state)
13106
13144
  return printContinue();
13107
13145
  if (state.status !== "pending" || now > state.expiresAt)
13108
13146
  return printContinue();
13109
- if (state.lastCheckedAt !== undefined && now - state.lastCheckedAt < cooldownMs()) {
13110
- return printContinue();
13111
- }
13112
13147
  const cfg = loadConfig();
13113
- if (!isAuthenticated(cfg) || !cfg.deployment_id)
13148
+ if (!cfg.api_key || !cfg.deployment_id)
13114
13149
  return printContinue();
13115
13150
  const tc = await Promise.resolve().then(() => (init_ticket_client(), exports_ticket_client));
13116
- let resp;
13117
- try {
13118
- resp = await tc.requestGetTicket(cfg, state.ticketId);
13119
- } catch {
13120
- saveState({ ...state, lastCheckedAt: now });
13121
- return printContinue();
13122
- }
13123
- if (resp.status === "ready" && resp.result) {
13124
- saveState({ ...state, status: "delivered", deliveredAt: now, lastCheckedAt: now });
13125
- if (resp.result.context.trim()) {
13126
- const { buildReadyEnvelope: buildReadyEnvelope2, STOP_PREFIX: STOP_PREFIX2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
13127
- const envelope = buildReadyEnvelope2(resp.result.context, resp.result.save_recommended ?? false);
13128
- console.log(JSON.stringify({ decision: "block", reason: `${STOP_PREFIX2}
13151
+ const pollMs = stopPollMs();
13152
+ const maxWaits = pollMs > 0 ? Math.floor(stopWaitMs() / pollMs) : 0;
13153
+ for (let waited = 0;; waited++) {
13154
+ let resp;
13155
+ try {
13156
+ resp = await tc.requestGetTicket(cfg, state.ticketId);
13157
+ } catch {
13158
+ saveState({ ...state, lastCheckedAt: now });
13159
+ return printContinue();
13160
+ }
13161
+ if (resp.status === "ready" && resp.result) {
13162
+ saveState({ ...state, status: "delivered", deliveredAt: now, lastCheckedAt: now });
13163
+ if (resp.result.context.trim()) {
13164
+ const { buildReadyEnvelope: buildReadyEnvelope2, STOP_PREFIX: STOP_PREFIX2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
13165
+ const envelope = buildReadyEnvelope2(resp.result.context, resp.result.save_recommended ?? false);
13166
+ console.log(JSON.stringify({ decision: "block", reason: `${STOP_PREFIX2}
13129
13167
 
13130
13168
  ${envelope}` }));
13131
- logger.info("hooks", `stop tid=${state.ticketId} delivered=true`);
13132
- return;
13169
+ logger.info("hooks", `stop tid=${state.ticketId} delivered=true`);
13170
+ return;
13171
+ }
13172
+ logger.debug("hooks", `stop tid=${state.ticketId} delivered=true gap-no-block`);
13173
+ return printContinue();
13133
13174
  }
13134
- logger.debug("hooks", `stop tid=${state.ticketId} delivered=true gap-no-block`);
13135
- return printContinue();
13175
+ if (resp.status !== "pending" || waited >= maxWaits)
13176
+ break;
13177
+ await sleep2(pollMs);
13136
13178
  }
13137
13179
  saveState({ ...state, lastCheckedAt: now });
13138
13180
  logger.debug("hooks", `stop tid=${state.ticketId} delivered=false`);
@@ -13210,7 +13252,7 @@ async function runInstall(agent, opts) {
13210
13252
  const { installClaudeHooks: installClaudeHooks2, claudeLocalSettingsPath: claudeLocalSettingsPath2 } = await Promise.resolve().then(() => (init_claude_code(), exports_claude_code));
13211
13253
  const configPath = claudeLocalSettingsPath2(resolveDir(opts.dir));
13212
13254
  try {
13213
- const { events } = installClaudeHooks2(configPath, { withStop: opts.withStop });
13255
+ const { events } = installClaudeHooks2(configPath, { stop: opts.stop });
13214
13256
  if (opts.json) {
13215
13257
  const { emitStep: emitStep2 } = await Promise.resolve().then(() => exports_output);
13216
13258
  emitStep2({ step: "hooks-install", path: configPath, events });
@@ -13300,7 +13342,7 @@ async function collectDoctorChecks(opts) {
13300
13342
  detail: "no deployment selected (run 'dosu setup')"
13301
13343
  });
13302
13344
  }
13303
- if (cfg.api_key && cfg.deployment_id && isAuthenticated(cfg)) {
13345
+ if (cfg.api_key && cfg.deployment_id) {
13304
13346
  const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), exports_client));
13305
13347
  const ok = await new Client2(cfg).validateAPIKey(cfg.api_key, cfg.deployment_id);
13306
13348
  checks.push({
@@ -13312,7 +13354,7 @@ async function collectDoctorChecks(opts) {
13312
13354
  checks.push({
13313
13355
  name: "backend",
13314
13356
  status: "warn",
13315
- detail: "skipped (not authenticated / no deployment)"
13357
+ detail: "skipped (no API key / no deployment)"
13316
13358
  });
13317
13359
  }
13318
13360
  return checks;
@@ -13342,7 +13384,7 @@ function hooksCommand() {
13342
13384
  cmd.command("post-tool-use").description("Hook entrypoint: poll and inject ready knowledge once").action(async () => {
13343
13385
  await runHookEntrypoint("post-tool-use", readStdinRaw());
13344
13386
  });
13345
- cmd.command("stop").description("Hook entrypoint (opt-in): last-chance non-blocking delivery").action(async () => {
13387
+ cmd.command("stop").description("Hook entrypoint: last-chance knowledge delivery when the agent stops").action(async () => {
13346
13388
  await runHookEntrypoint("stop", readStdinRaw());
13347
13389
  });
13348
13390
  cmd.command("status").description("Show the active Dosu knowledge ticket for this session").option("--json", "Output JSON", false).action((opts) => {
@@ -13355,13 +13397,13 @@ function hooksCommand() {
13355
13397
  }
13356
13398
  runStatus(input, opts);
13357
13399
  });
13358
- cmd.command("install").description("Install Dosu hooks into a coding agent's local config").addArgument(new Argument("<agent>", "coding agent to configure").choices(SUPPORTED_AGENTS)).option("--scope <scope>", "Config scope (local only)", "local").option("--dir <path>", "Project root (defaults to current directory)").option("--with-stop", "Also install the optional Stop hook", false).option("--json", "Emit machine-readable JSON", false).addHelpText("after", [
13400
+ cmd.command("install").description("Install Dosu hooks into a coding agent's local config").addArgument(new Argument("<agent>", "coding agent to configure").choices(SUPPORTED_AGENTS)).option("--scope <scope>", "Config scope (local only)", "local").option("--dir <path>", "Project root (defaults to current directory)").option("--no-stop", "Skip the Stop hook (knowledge then delivers mid-session only, less reliably)").option("--json", "Emit machine-readable JSON", false).addHelpText("after", [
13359
13401
  "",
13360
13402
  `Supported agents: ${SUPPORTED_AGENTS.join(", ")}`,
13361
13403
  "",
13362
13404
  "Examples:",
13363
13405
  " $ dosu hooks install claude-code",
13364
- " $ dosu hooks install claude-code --with-stop",
13406
+ " $ dosu hooks install claude-code --no-stop",
13365
13407
  " $ dosu hooks install claude-code --dir ./my-project"
13366
13408
  ].join(`
13367
13409
  `)).action((agent, opts) => runInstall(agent, opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Manage MCP servers for AI tools",
6
6
  "license": "MIT",