@dosu/cli 0.38.0 → 0.40.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 +103 -26
  2. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -4759,7 +4759,7 @@ var init_dist4 = __esm(() => {
4759
4759
  function getVersionString() {
4760
4760
  return `v${VERSION}`;
4761
4761
  }
4762
- var VERSION = "0.38.0", INSTALL_CHANNEL = "npm";
4762
+ var VERSION = "0.40.0", INSTALL_CHANNEL = "npm";
4763
4763
 
4764
4764
  // src/debug/logger.ts
4765
4765
  import {
@@ -8178,7 +8178,8 @@ __export(exports_prompts, {
8178
8178
  buildReadyEnvelope: () => buildReadyEnvelope,
8179
8179
  STOP_PREFIX: () => STOP_PREFIX,
8180
8180
  SAVE_NUDGE: () => SAVE_NUDGE,
8181
- LOOKUP_STARTED_NOTE: () => LOOKUP_STARTED_NOTE
8181
+ LOOKUP_STARTED_NOTE: () => LOOKUP_STARTED_NOTE,
8182
+ LATE_RESULT_NOTE: () => LATE_RESULT_NOTE
8182
8183
  });
8183
8184
  function buildReadyEnvelope(context, saveRecommended = false) {
8184
8185
  const trimmed = context.trim();
@@ -8204,9 +8205,10 @@ function buildReadyEnvelope(context, saveRecommended = false) {
8204
8205
 
8205
8206
  `);
8206
8207
  }
8207
- var LOOKUP_STARTED_NOTE, STOP_PREFIX, SAVE_NUDGE;
8208
+ var LOOKUP_STARTED_NOTE, LATE_RESULT_NOTE, STOP_PREFIX, SAVE_NUDGE;
8208
8209
  var init_prompts = __esm(() => {
8209
8210
  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.";
8211
+ LATE_RESULT_NOTE = "Heads-up: the context below answers your PREVIOUS prompt — its lookup finished after " + "that turn had already ended. Apply it only where it is still relevant to the current " + "work; do not treat it as a response to the new prompt.";
8210
8212
  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.";
8211
8213
  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.";
8212
8214
  });
@@ -15033,12 +15035,13 @@ init_commander();
15033
15035
  init_config();
15034
15036
  init_logger();
15035
15037
  init_prompts();
15038
+ import { execFileSync as execFileSync2 } from "node:child_process";
15036
15039
  import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
15037
15040
  import { basename } from "node:path";
15038
15041
 
15039
15042
  // src/hooks/state.ts
15040
15043
  init_config();
15041
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
15044
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
15042
15045
  import { dirname as dirname2, join as join5 } from "node:path";
15043
15046
  function getStateDir() {
15044
15047
  return process.env.DOSU_HOOK_STATE_DIR ?? join5(getConfigDir(), "hooks");
@@ -15066,9 +15069,24 @@ function saveState(state) {
15066
15069
  writeFileSync5(file, JSON.stringify(state, null, 2), { mode: 384 });
15067
15070
  } catch {}
15068
15071
  }
15069
- function clearState(sessionId) {
15072
+ function claimState(sessionId) {
15073
+ const file = stateFile(sessionId);
15070
15074
  try {
15071
- rmSync2(stateFile(sessionId), { force: true });
15075
+ renameSync2(file, `${file}.claim`);
15076
+ return JSON.parse(readFileSync6(`${file}.claim`, "utf8"));
15077
+ } catch {
15078
+ return null;
15079
+ }
15080
+ }
15081
+ function releaseState(sessionId, state) {
15082
+ const file = stateFile(sessionId);
15083
+ if (state) {
15084
+ try {
15085
+ writeFileSync5(file, JSON.stringify(state, null, 2), { flag: "wx", mode: 384 });
15086
+ } catch {}
15087
+ }
15088
+ try {
15089
+ rmSync2(`${file}.claim`, { force: true });
15072
15090
  } catch {}
15073
15091
  }
15074
15092
 
@@ -15111,6 +15129,27 @@ function errMsg(err) {
15111
15129
  function repoSlug(cwd) {
15112
15130
  return cwd ? basename(cwd) : undefined;
15113
15131
  }
15132
+ function gitContext(cwd) {
15133
+ if (!cwd)
15134
+ return {};
15135
+ const run2 = (args) => {
15136
+ try {
15137
+ const out = execFileSync2("git", args, {
15138
+ cwd,
15139
+ timeout: 1500,
15140
+ encoding: "utf8",
15141
+ stdio: ["ignore", "pipe", "ignore"]
15142
+ });
15143
+ return out.toString().trim() || undefined;
15144
+ } catch {
15145
+ return;
15146
+ }
15147
+ };
15148
+ const repo = run2(["remote", "get-url", "origin"]);
15149
+ if (!repo)
15150
+ return {};
15151
+ return { repo, branch: run2(["branch", "--show-current"]) };
15152
+ }
15114
15153
  async function runUserPromptSubmit(input, now = Date.now(), agent = DEFAULT_AGENT) {
15115
15154
  const sessionId = input.session_id;
15116
15155
  if (!sessionId)
@@ -15128,14 +15167,31 @@ async function runUserPromptSubmit(input, now = Date.now(), agent = DEFAULT_AGEN
15128
15167
  logger.debug("hooks", `submit sid=${sid8(sessionId)} reuse tid=${existing.ticketId}`);
15129
15168
  return;
15130
15169
  }
15131
- if (existing)
15132
- clearState(sessionId);
15170
+ const claimed = existing ? claimState(sessionId) : null;
15133
15171
  const cfg = loadConfig();
15134
15172
  if (!cfg.active_account?.target?.api_key || !cfg.active_account?.target?.deployment_id) {
15173
+ if (claimed)
15174
+ releaseState(sessionId, null);
15135
15175
  logger.debug("hooks", "submit skipped reason=not-configured");
15136
15176
  return;
15137
15177
  }
15178
+ const git = gitContext(input.cwd);
15138
15179
  const tc = await Promise.resolve().then(() => (init_ticket_client(), exports_ticket_client));
15180
+ let lateBlock;
15181
+ if (claimed && claimed.status === "pending" && now <= claimed.expiresAt) {
15182
+ try {
15183
+ const old = await tc.requestGetTicket(cfg, claimed.ticketId);
15184
+ if (old.status === "ready" && old.result?.context.trim()) {
15185
+ const { buildReadyEnvelope: buildReadyEnvelope2, LATE_RESULT_NOTE: LATE_RESULT_NOTE2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
15186
+ lateBlock = `${LATE_RESULT_NOTE2}
15187
+
15188
+ ${buildReadyEnvelope2(old.result.context, old.result.save_recommended ?? false)}`;
15189
+ logger.info("hooks", `harvest tid=${claimed.ticketId} delivered=late`);
15190
+ }
15191
+ } catch {}
15192
+ }
15193
+ if (claimed)
15194
+ releaseState(sessionId, null);
15139
15195
  const startedAt = Date.now();
15140
15196
  let resp;
15141
15197
  try {
@@ -15145,7 +15201,8 @@ async function runUserPromptSubmit(input, now = Date.now(), agent = DEFAULT_AGEN
15145
15201
  session_id: sessionId,
15146
15202
  turn_id: turnId,
15147
15203
  prompt,
15148
- repo: repoSlug(input.cwd)
15204
+ repo: git.repo ?? repoSlug(input.cwd),
15205
+ branch: git.branch
15149
15206
  });
15150
15207
  } catch (err) {
15151
15208
  logger.warn("hooks", `error event=user-prompt-submit reason=${errMsg(err)}`);
@@ -15161,7 +15218,9 @@ async function runUserPromptSubmit(input, now = Date.now(), agent = DEFAULT_AGEN
15161
15218
  });
15162
15219
  logger.info("hooks", `create sid=${sid8(sessionId)} tid=${resp.ticket_id} latency=${Date.now() - startedAt}ms`);
15163
15220
  const { LOOKUP_STARTED_NOTE: LOOKUP_STARTED_NOTE2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
15164
- printHookContext("UserPromptSubmit", LOOKUP_STARTED_NOTE2);
15221
+ printHookContext("UserPromptSubmit", lateBlock ? `${lateBlock}
15222
+
15223
+ ${LOOKUP_STARTED_NOTE2}` : LOOKUP_STARTED_NOTE2);
15165
15224
  }
15166
15225
  async function runPostToolUse(input, now = Date.now()) {
15167
15226
  const sessionId = input.session_id;
@@ -15189,35 +15248,45 @@ async function runPostToolUse(input, now = Date.now()) {
15189
15248
  saveState({ ...state, lastCheckedAt: now });
15190
15249
  return;
15191
15250
  }
15251
+ const claimed = claimState(sessionId);
15252
+ if (!claimed) {
15253
+ logger.debug("hooks", `poll tid=${state.ticketId} skipped=contended`);
15254
+ return;
15255
+ }
15192
15256
  const tc = await Promise.resolve().then(() => (init_ticket_client(), exports_ticket_client));
15193
15257
  const startedAt = Date.now();
15194
15258
  let resp;
15195
15259
  try {
15196
- resp = await tc.requestGetTicket(cfg, state.ticketId);
15260
+ resp = await tc.requestGetTicket(cfg, claimed.ticketId);
15197
15261
  } catch (err) {
15198
15262
  if (tc.isDefinitiveError(err)) {
15199
- saveState({ ...state, status: "failed", lastCheckedAt: now });
15263
+ releaseState(sessionId, { ...claimed, status: "failed", lastCheckedAt: now });
15200
15264
  logger.warn("hooks", `error event=post-tool-use reason=${errMsg(err)} definitive`);
15201
15265
  } else {
15202
- saveState({ ...state, lastCheckedAt: now });
15203
- logger.debug("hooks", `poll tid=${state.ticketId} transient-error`);
15266
+ releaseState(sessionId, { ...claimed, lastCheckedAt: now });
15267
+ logger.debug("hooks", `poll tid=${claimed.ticketId} transient-error`);
15204
15268
  }
15205
15269
  return;
15206
15270
  }
15207
15271
  if (resp.status === "pending") {
15208
- saveState({ ...state, lastCheckedAt: now });
15209
- logger.debug("hooks", `poll tid=${state.ticketId} status=pending`);
15272
+ releaseState(sessionId, { ...claimed, lastCheckedAt: now });
15273
+ logger.debug("hooks", `poll tid=${claimed.ticketId} status=pending`);
15210
15274
  return;
15211
15275
  }
15212
15276
  if (resp.status === "failed" || resp.status === "expired") {
15213
- saveState({ ...state, status: resp.status, lastCheckedAt: now });
15277
+ releaseState(sessionId, { ...claimed, status: resp.status, lastCheckedAt: now });
15214
15278
  return;
15215
15279
  }
15216
15280
  if (!resp.result) {
15217
- saveState({ ...state, status: "failed", lastCheckedAt: now });
15281
+ releaseState(sessionId, { ...claimed, status: "failed", lastCheckedAt: now });
15218
15282
  return;
15219
15283
  }
15220
- saveState({ ...state, status: "delivered", deliveredAt: now, lastCheckedAt: now });
15284
+ releaseState(sessionId, {
15285
+ ...claimed,
15286
+ status: "delivered",
15287
+ deliveredAt: now,
15288
+ lastCheckedAt: now
15289
+ });
15221
15290
  const { buildReadyEnvelope: buildReadyEnvelope2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
15222
15291
  const context = buildReadyEnvelope2(resp.result.context, resp.result.save_recommended ?? false);
15223
15292
  if (context) {
@@ -15239,37 +15308,45 @@ async function runStop(input, now = Date.now()) {
15239
15308
  const cfg = loadConfig();
15240
15309
  if (!cfg.active_account?.target?.api_key || !cfg.active_account?.target?.deployment_id)
15241
15310
  return printContinue();
15311
+ const claimed = claimState(sessionId);
15312
+ if (!claimed)
15313
+ return printContinue();
15242
15314
  const tc = await Promise.resolve().then(() => (init_ticket_client(), exports_ticket_client));
15243
15315
  const pollMs = stopPollMs();
15244
15316
  const maxWaits = pollMs > 0 ? Math.floor(stopWaitMs() / pollMs) : 0;
15245
15317
  for (let waited = 0;; waited++) {
15246
15318
  let resp;
15247
15319
  try {
15248
- resp = await tc.requestGetTicket(cfg, state.ticketId);
15320
+ resp = await tc.requestGetTicket(cfg, claimed.ticketId);
15249
15321
  } catch {
15250
- saveState({ ...state, lastCheckedAt: now });
15322
+ releaseState(sessionId, { ...claimed, lastCheckedAt: now });
15251
15323
  return printContinue();
15252
15324
  }
15253
15325
  if (resp.status === "ready" && resp.result) {
15254
- saveState({ ...state, status: "delivered", deliveredAt: now, lastCheckedAt: now });
15326
+ releaseState(sessionId, {
15327
+ ...claimed,
15328
+ status: "delivered",
15329
+ deliveredAt: now,
15330
+ lastCheckedAt: now
15331
+ });
15255
15332
  if (resp.result.context.trim()) {
15256
15333
  const { buildReadyEnvelope: buildReadyEnvelope2, STOP_PREFIX: STOP_PREFIX2 } = await Promise.resolve().then(() => (init_prompts(), exports_prompts));
15257
15334
  const envelope = buildReadyEnvelope2(resp.result.context, resp.result.save_recommended ?? false);
15258
15335
  console.log(JSON.stringify({ decision: "block", reason: `${STOP_PREFIX2}
15259
15336
 
15260
15337
  ${envelope}` }));
15261
- logger.info("hooks", `stop tid=${state.ticketId} delivered=true`);
15338
+ logger.info("hooks", `stop tid=${claimed.ticketId} delivered=true`);
15262
15339
  return;
15263
15340
  }
15264
- logger.debug("hooks", `stop tid=${state.ticketId} delivered=true gap-no-block`);
15341
+ logger.debug("hooks", `stop tid=${claimed.ticketId} delivered=true gap-no-block`);
15265
15342
  return printContinue();
15266
15343
  }
15267
15344
  if (resp.status !== "pending" || waited >= maxWaits)
15268
15345
  break;
15269
15346
  await sleep4(pollMs);
15270
15347
  }
15271
- saveState({ ...state, lastCheckedAt: now });
15272
- logger.debug("hooks", `stop tid=${state.ticketId} delivered=false`);
15348
+ releaseState(sessionId, { ...claimed, lastCheckedAt: now });
15349
+ logger.debug("hooks", `stop tid=${claimed.ticketId} delivered=false`);
15273
15350
  printContinue();
15274
15351
  }
15275
15352
  function runStatus(input, opts, now = Date.now()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Manage MCP servers for AI tools",
6
6
  "license": "MIT",