@adhdev/daemon-core 0.9.54 → 0.9.55

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.
package/dist/index.mjs CHANGED
@@ -1940,6 +1940,7 @@ function resolveCliAdapterConfig(provider) {
1940
1940
  sendDelayMs: typeof provider.sendDelayMs === "number" ? Math.max(0, provider.sendDelayMs) : 0,
1941
1941
  sendKey: typeof provider.sendKey === "string" && provider.sendKey.length > 0 ? provider.sendKey : "\r",
1942
1942
  submitStrategy: provider.submitStrategy === "immediate" ? "immediate" : "wait_for_echo",
1943
+ requirePromptEchoBeforeSubmit: provider.requirePromptEchoBeforeSubmit === true,
1943
1944
  providerResolutionMeta: {
1944
1945
  type: provider.type,
1945
1946
  name: provider.name,
@@ -2204,6 +2205,7 @@ var init_provider_cli_adapter = __esm({
2204
2205
  this.sendDelayMs = resolvedConfig.sendDelayMs;
2205
2206
  this.sendKey = resolvedConfig.sendKey;
2206
2207
  this.submitStrategy = resolvedConfig.submitStrategy;
2208
+ this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
2207
2209
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
2208
2210
  this.cliScripts = provider.scripts || {};
2209
2211
  const scriptNames = listCliScriptNames(this.cliScripts);
@@ -2500,6 +2502,7 @@ var init_provider_cli_adapter = __esm({
2500
2502
  sendDelayMs;
2501
2503
  sendKey;
2502
2504
  submitStrategy;
2505
+ requirePromptEchoBeforeSubmit;
2503
2506
  static SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
2504
2507
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
2505
2508
  setCliScripts(scripts) {
@@ -4020,6 +4023,22 @@ var init_provider_cli_adapter = __esm({
4020
4023
  }
4021
4024
  }
4022
4025
  if (elapsed >= state.maxEchoWaitMs) {
4026
+ const diagnostic = {
4027
+ elapsed,
4028
+ maxEchoWaitMs: state.maxEchoWaitMs,
4029
+ submitDelayMs: state.submitDelayMs,
4030
+ promptSnippet: state.normalizedPromptSnippet,
4031
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4032
+ screenText: summarizeCliTraceText(screenText, 1e3)
4033
+ };
4034
+ this.recordTrace("submit_echo_missing", diagnostic);
4035
+ if (this.requirePromptEchoBeforeSubmit) {
4036
+ const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
4037
+ LOG.warn("CLI", `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
4038
+ completion.rejectOnce(new Error(message));
4039
+ return;
4040
+ }
4041
+ LOG.warn("CLI", `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
4023
4042
  this.submitSendKey(state, completion);
4024
4043
  return;
4025
4044
  }
@@ -4484,6 +4503,7 @@ var init_provider_cli_adapter = __esm({
4484
4503
  sendDelayMs: this.sendDelayMs,
4485
4504
  sendKey: this.sendKey,
4486
4505
  submitStrategy: this.submitStrategy,
4506
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4487
4507
  submitPendingUntil: this.submitPendingUntil,
4488
4508
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
4489
4509
  resizeSuppressUntil: this.resizeSuppressUntil,
@@ -5406,13 +5426,8 @@ var GIT_COMMAND_NAMES = /* @__PURE__ */ new Set([
5406
5426
  "git_checkpoint",
5407
5427
  "git_stash_push",
5408
5428
  "git_stash_pop",
5409
- "git_checkout_files"
5410
- ]);
5411
- var MUTATING_COMMAND_NAMES = /* @__PURE__ */ new Set([
5412
- "git_checkpoint",
5413
- "git_stash_push",
5414
- "git_stash_pop",
5415
- "git_checkout_files"
5429
+ "git_checkout_files",
5430
+ "git_remote_url"
5416
5431
  ]);
5417
5432
  var SNAPSHOT_REASONS = /* @__PURE__ */ new Set([
5418
5433
  "session_baseline",
@@ -5453,7 +5468,12 @@ function createDefaultGitCommandServices() {
5453
5468
  turnId
5454
5469
  }),
5455
5470
  compareSnapshots: ({ beforeSnapshotId, afterSnapshotId }) => defaultSnapshotStore.compare(beforeSnapshotId, afterSnapshotId),
5456
- getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until })
5471
+ getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until }),
5472
+ checkpoint: async ({ workspace, message, includeUntracked = false }) => gitCheckpoint(workspace, message, includeUntracked),
5473
+ stashPush: async ({ workspace, message, includeUntracked = false }) => gitStashPush(workspace, message, includeUntracked),
5474
+ stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
5475
+ checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
5476
+ getRemoteUrl: async ({ workspace, remote = "origin" }) => gitGetRemoteUrl(workspace, remote)
5457
5477
  };
5458
5478
  }
5459
5479
  var defaultGitCommandServices = createDefaultGitCommandServices();
@@ -5517,9 +5537,6 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5517
5537
  if (!isGitCommandName(command)) {
5518
5538
  return failure("invalid_args", `Unknown Git command: ${command}`);
5519
5539
  }
5520
- if (MUTATING_COMMAND_NAMES.has(command)) {
5521
- return failure("invalid_args", `${command} is not implemented in daemon-core read-only Git routing`);
5522
- }
5523
5540
  const workspaceResult = validateWorkspace2(args);
5524
5541
  if ("success" in workspaceResult) return workspaceResult;
5525
5542
  const { workspace } = workspaceResult;
@@ -5579,10 +5596,153 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5579
5596
  }));
5580
5597
  return "success" in log ? log : { success: true, log };
5581
5598
  }
5599
+ case "git_checkpoint": {
5600
+ if (!services.checkpoint) return serviceNotImplemented(command);
5601
+ const msg = validateMutatingMessage(args?.message);
5602
+ if (typeof msg !== "string") return msg;
5603
+ const includeUntracked = Boolean(args?.includeUntracked);
5604
+ const checkpoint = await runService(() => services.checkpoint({ workspace, message: msg, includeUntracked }));
5605
+ return "success" in checkpoint ? checkpoint : { success: true, checkpoint };
5606
+ }
5607
+ case "git_stash_push": {
5608
+ if (!services.stashPush) return serviceNotImplemented(command);
5609
+ const msg = validateMutatingMessage(args?.message);
5610
+ if (typeof msg !== "string") return msg;
5611
+ const includeUntracked = Boolean(args?.includeUntracked);
5612
+ const stash = await runService(() => services.stashPush({ workspace, message: msg, includeUntracked }));
5613
+ return "success" in stash ? stash : { success: true, stash };
5614
+ }
5615
+ case "git_stash_pop": {
5616
+ if (!services.stashPop) return serviceNotImplemented(command);
5617
+ const stashRef = optionalString(args?.stashRef);
5618
+ if (stashRef !== void 0 && !/^stash@\{\d+\}$/.test(stashRef)) {
5619
+ return failure("invalid_args", "stashRef must match stash@{N} format");
5620
+ }
5621
+ const popResult = await runService(() => services.stashPop({ workspace, stashRef }));
5622
+ if (popResult !== void 0 && "success" in popResult) return popResult;
5623
+ return { success: true, stashPopped: true };
5624
+ }
5625
+ case "git_checkout_files": {
5626
+ if (!services.checkoutFiles) return serviceNotImplemented(command);
5627
+ const paths = args?.paths;
5628
+ if (!Array.isArray(paths) || paths.length === 0) {
5629
+ return failure("invalid_args", "paths must be a non-empty array");
5630
+ }
5631
+ if (paths.length > 50) {
5632
+ return failure("invalid_args", "paths array exceeds maximum of 50 entries");
5633
+ }
5634
+ const checkoutResult = await runService(() => services.checkoutFiles({ workspace, paths }));
5635
+ return "success" in checkoutResult ? checkoutResult : { success: true, checkedOut: checkoutResult.checkedOut };
5636
+ }
5637
+ case "git_remote_url": {
5638
+ if (!services.getRemoteUrl) return serviceNotImplemented(command);
5639
+ const remote = typeof args?.remote === "string" && args.remote.trim() ? args.remote.trim() : "origin";
5640
+ const remoteResult = await runService(() => services.getRemoteUrl({ workspace, remote }));
5641
+ if ("success" in remoteResult) return remoteResult;
5642
+ return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
5643
+ }
5582
5644
  default:
5583
5645
  return failure("invalid_args", `Unknown Git command: ${command}`);
5584
5646
  }
5585
5647
  }
5648
+ function validateMutatingMessage(value) {
5649
+ if (typeof value !== "string" || !value.trim()) {
5650
+ return failure("invalid_args", "message must be a non-empty string");
5651
+ }
5652
+ const msg = value.trim();
5653
+ if (msg.length > 200) {
5654
+ return failure("invalid_args", "message must be 200 characters or fewer");
5655
+ }
5656
+ return msg;
5657
+ }
5658
+ async function gitCheckpoint(workspace, message, includeUntracked) {
5659
+ const repo = await resolveGitRepository(workspace);
5660
+ const repoRoot = repo.repoRoot;
5661
+ const statusResult = await getGitRepoStatus(workspace);
5662
+ if (statusResult.hasConflicts) {
5663
+ throw new GitCommandError("conflict", "Repository has conflicts \u2014 resolve before checkpointing");
5664
+ }
5665
+ const addArgs = includeUntracked ? ["-A"] : ["-u"];
5666
+ await runGit(repo, ["add", ...addArgs], { cwd: repoRoot });
5667
+ const fullMsg = `adhdev: checkpoint ${message}`;
5668
+ let commitSha;
5669
+ try {
5670
+ await runGit(repo, ["commit", "-m", fullMsg], { cwd: repoRoot });
5671
+ const revResult = await runGit(repo, ["rev-parse", "HEAD"], { cwd: repoRoot });
5672
+ commitSha = revResult.stdout.trim();
5673
+ } catch (err) {
5674
+ const output = (err?.stdout || "") + (err?.stderr || "");
5675
+ if (/nothing to commit/i.test(output)) {
5676
+ throw new GitCommandError("git_command_failed", "Nothing to commit");
5677
+ }
5678
+ throw err;
5679
+ }
5680
+ return {
5681
+ workspace: repo.workspace,
5682
+ repoRoot,
5683
+ isGitRepo: true,
5684
+ commit: commitSha,
5685
+ message: fullMsg,
5686
+ lastCheckedAt: Date.now()
5687
+ };
5688
+ }
5689
+ async function gitStashPush(workspace, message, includeUntracked) {
5690
+ const repo = await resolveGitRepository(workspace);
5691
+ const repoRoot = repo.repoRoot;
5692
+ const stashArgs = ["stash", "push", "-m", message];
5693
+ if (includeUntracked) stashArgs.push("--include-untracked");
5694
+ const result = await runGit(repo, stashArgs, { cwd: repoRoot });
5695
+ if (/No local changes to save/i.test(result.stdout + result.stderr)) {
5696
+ throw new GitCommandError("git_command_failed", "Nothing to stash");
5697
+ }
5698
+ return {
5699
+ workspace: repo.workspace,
5700
+ repoRoot,
5701
+ isGitRepo: true,
5702
+ stashRef: "stash@{0}",
5703
+ message,
5704
+ lastCheckedAt: Date.now()
5705
+ };
5706
+ }
5707
+ async function gitStashPop(workspace, stashRef) {
5708
+ const repo = await resolveGitRepository(workspace);
5709
+ const repoRoot = repo.repoRoot;
5710
+ const popArgs = stashRef ? ["stash", "pop", stashRef] : ["stash", "pop"];
5711
+ await runGit(repo, popArgs, { cwd: repoRoot });
5712
+ }
5713
+ async function gitCheckoutFiles(workspace, paths) {
5714
+ const repo = await resolveGitRepository(workspace);
5715
+ const repoRoot = repo.repoRoot;
5716
+ const normalizedPaths = [];
5717
+ for (const p of paths) {
5718
+ if (typeof p !== "string" || !p.trim() || p.includes("\0")) {
5719
+ throw new GitCommandError("invalid_args", `Invalid path: ${String(p)}`);
5720
+ }
5721
+ if (path3.isAbsolute(p)) {
5722
+ throw new GitCommandError("invalid_args", `Path must be repository-relative, not absolute: ${p}`);
5723
+ }
5724
+ const normalized = path3.normalize(p.trim()).split(path3.sep).join("/");
5725
+ if (normalized.startsWith("../") || normalized === "..") {
5726
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5727
+ }
5728
+ const absolutePath = path3.resolve(repoRoot, normalized);
5729
+ if (!isPathInside(repoRoot, absolutePath)) {
5730
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5731
+ }
5732
+ normalizedPaths.push(normalized);
5733
+ }
5734
+ await runGit(repo, ["checkout", "--", ...normalizedPaths], { cwd: repoRoot });
5735
+ return { checkedOut: normalizedPaths };
5736
+ }
5737
+ async function gitGetRemoteUrl(workspace, remote) {
5738
+ const repo = await resolveGitRepository(workspace);
5739
+ const result = await runGit(repo, ["remote", "get-url", remote], { cwd: repo.repoRoot });
5740
+ const remoteUrl = result.stdout.trim();
5741
+ if (!remoteUrl) {
5742
+ throw new GitCommandError("git_command_failed", `Remote '${remote}' has no URL`);
5743
+ }
5744
+ return { remoteUrl, remote };
5745
+ }
5586
5746
  function formatOptionalGitLogRangeArg(flag, value) {
5587
5747
  return value ? [`${flag}=${value}`] : [];
5588
5748
  }
@@ -5641,6 +5801,27 @@ function validateGitLogPath(repoRoot, filePath) {
5641
5801
  return normalized;
5642
5802
  }
5643
5803
 
5804
+ // src/git/turn-snapshot-tracker.ts
5805
+ var BUSY_STATUSES = /* @__PURE__ */ new Set(["streaming", "waiting_approval"]);
5806
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error"]);
5807
+ var TurnSnapshotTracker = class {
5808
+ lastStatus = /* @__PURE__ */ new Map();
5809
+ onTurnCompleted;
5810
+ constructor(onTurnCompleted) {
5811
+ this.onTurnCompleted = onTurnCompleted;
5812
+ }
5813
+ record(sessionId, status, workspace) {
5814
+ const prev = this.lastStatus.get(sessionId);
5815
+ this.lastStatus.set(sessionId, status);
5816
+ if (workspace && prev && BUSY_STATUSES.has(prev) && TERMINAL_STATUSES.has(status)) {
5817
+ this.onTurnCompleted({ sessionId, workspace });
5818
+ }
5819
+ }
5820
+ forget(sessionId) {
5821
+ this.lastStatus.delete(sessionId);
5822
+ }
5823
+ };
5824
+
5644
5825
  // src/index.ts
5645
5826
  init_config();
5646
5827
 
@@ -14528,6 +14709,16 @@ var DaemonCommandHandler = class {
14528
14709
  return result;
14529
14710
  }
14530
14711
  }
14712
+ if (cmd === "send_chat" && this._ctx.onBeforeSendChat) {
14713
+ const sessionId = this._currentRoute.session?.sessionId;
14714
+ const workspace = sessionId ? this._ctx.instanceManager?.getInstance(sessionId)?.getState?.()?.workspace : void 0;
14715
+ if (workspace && sessionId) {
14716
+ try {
14717
+ this._ctx.onBeforeSendChat({ workspace, sessionId });
14718
+ } catch {
14719
+ }
14720
+ }
14721
+ }
14531
14722
  try {
14532
14723
  result = await this.dispatch(cmd, args);
14533
14724
  this.logCommandEnd(cmd, result, startedAt);
@@ -17768,6 +17959,7 @@ var KNOWN_PROVIDER_FIELDS = /* @__PURE__ */ new Set([
17768
17959
  "sendDelayMs",
17769
17960
  "sendKey",
17770
17961
  "submitStrategy",
17962
+ "requirePromptEchoBeforeSubmit",
17771
17963
  "timeouts",
17772
17964
  "disableUpstream"
17773
17965
  ]);
@@ -29292,6 +29484,7 @@ async function initDaemonComponents(config) {
29292
29484
  providerLoader,
29293
29485
  instanceManager,
29294
29486
  sessionRegistry,
29487
+ gitCommandServices: createDefaultGitCommandServices(),
29295
29488
  onProviderSettingChanged: async (providerType) => {
29296
29489
  await refreshProviderAvailability(providerType);
29297
29490
  config.onStatusChange?.();
@@ -29299,7 +29492,8 @@ async function initDaemonComponents(config) {
29299
29492
  onProviderSourceConfigChanged: async () => {
29300
29493
  await refreshProviderAvailability();
29301
29494
  config.onStatusChange?.();
29302
- }
29495
+ },
29496
+ onBeforeSendChat: config.onBeforeSendChat
29303
29497
  });
29304
29498
  agentStreamManager = new DaemonAgentStreamManager(
29305
29499
  LOG.forComponent("AgentStream").asLogFn(),
@@ -29450,6 +29644,7 @@ export {
29450
29644
  ProviderLoader,
29451
29645
  STANDALONE_CDP_SCAN_INTERVAL_MS,
29452
29646
  SessionHostPtyTransportFactory,
29647
+ TurnSnapshotTracker,
29453
29648
  VersionArchive,
29454
29649
  appendRecentActivity,
29455
29650
  buildAssistantChatMessage,