@praneeth_54/agentdoctor 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ import { colors, symbolFail, symbolOk } from "../utils/colors.js";
2
+ const STAGE_LABELS = {
3
+ understand: "Understanding question",
4
+ search: "Searching project knowledge",
5
+ retrieve: "Retrieving relevant evidence",
6
+ verify: "Verifying evidence",
7
+ prepare: "Preparing answer",
8
+ };
9
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10
+ /**
11
+ * Human-readable ask progress. Stages must be driven by real operations.
12
+ * Animation uses setInterval only for frame updates while work is in flight —
13
+ * never to invent delay.
14
+ */
15
+ export function createAskProgress(options = {}) {
16
+ const enabled = options.enabled !== false;
17
+ const stream = options.stream ?? process.stderr;
18
+ const tty = options.isTTY ??
19
+ Boolean(stream && typeof stream === "object" && "isTTY" in stream && stream.isTTY);
20
+ let frame = 0;
21
+ let timer;
22
+ let activeLabel;
23
+ let stopped = false;
24
+ const clearLine = () => {
25
+ if (!tty || !enabled)
26
+ return;
27
+ stream.write("\r\x1b[K");
28
+ };
29
+ const stopSpin = () => {
30
+ if (timer) {
31
+ clearInterval(timer);
32
+ timer = undefined;
33
+ }
34
+ clearLine();
35
+ };
36
+ const renderSpin = () => {
37
+ if (!tty || !enabled || !activeLabel || stopped)
38
+ return;
39
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
40
+ frame += 1;
41
+ stream.write(`\r${colors.cyan(glyph)} ${activeLabel}...`);
42
+ };
43
+ const writeDone = (ok, label, detail) => {
44
+ if (!enabled || stopped)
45
+ return;
46
+ const mark = ok ? symbolOk() : symbolFail();
47
+ const suffix = detail ? colors.dim(` — ${detail}`) : "";
48
+ stream.write(`${mark} ${label}${suffix}\n`);
49
+ };
50
+ return {
51
+ stage(id, state, detail) {
52
+ if (!enabled || stopped)
53
+ return;
54
+ const label = STAGE_LABELS[id];
55
+ if (state === "start") {
56
+ stopSpin();
57
+ activeLabel = label;
58
+ if (tty) {
59
+ frame = 0;
60
+ renderSpin();
61
+ timer = setInterval(renderSpin, 80);
62
+ timer.unref?.();
63
+ }
64
+ else {
65
+ stream.write(`${label}...\n`);
66
+ }
67
+ return;
68
+ }
69
+ stopSpin();
70
+ activeLabel = undefined;
71
+ writeDone(state === "ok", state === "ok" && id === "prepare" ? "Answer ready" : label, detail);
72
+ },
73
+ stop() {
74
+ stopSpin();
75
+ activeLabel = undefined;
76
+ stopped = true;
77
+ },
78
+ };
79
+ }
80
+ export function silentAskProgress() {
81
+ return {
82
+ stage() { },
83
+ stop() { },
84
+ };
85
+ }
@@ -5,6 +5,12 @@ export declare function runAskCommand(options: {
5
5
  json?: boolean;
6
6
  /** Test-only: force mock provider */
7
7
  useMock?: boolean;
8
+ /** Test override: force progress enabled/disabled */
9
+ progressEnabled?: boolean;
10
+ /** Test override: progress stream */
11
+ progressStream?: NodeJS.WritableStream;
12
+ /** Test override: treat progress stream as TTY */
13
+ progressIsTTY?: boolean;
8
14
  }): Promise<ExitCode>;
9
15
  export declare function runChatCommand(options: {
10
16
  root?: string;
@@ -5,6 +5,7 @@ import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/i
5
5
  import { EXIT_CODES } from "../../types/index.js";
6
6
  import { resolveCliProjectRoot } from "../safe-root.js";
7
7
  import { colors } from "../../utils/colors.js";
8
+ import { createAskProgress } from "../ask-progress.js";
8
9
  function printBanner(options) {
9
10
  const lines = [
10
11
  "",
@@ -58,13 +59,21 @@ export async function runAskCommand(options) {
58
59
  }
59
60
  const root = gated.root;
60
61
  const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
62
+ const progressEnabled = options.progressEnabled ?? !options.json;
63
+ const progress = createAskProgress({
64
+ enabled: progressEnabled,
65
+ ...(options.progressStream ? { stream: options.progressStream } : {}),
66
+ ...(options.progressIsTTY !== undefined ? { isTTY: options.progressIsTTY } : {}),
67
+ });
61
68
  const chat = new ChatService({
62
69
  root,
63
70
  provider,
64
71
  persistAudit: !options.useMock,
72
+ progress,
65
73
  });
66
74
  try {
67
75
  const response = await chat.ask(options.question);
76
+ progress.stop();
68
77
  if (options.json) {
69
78
  process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
70
79
  }
@@ -78,7 +87,14 @@ export async function runAskCommand(options) {
78
87
  }
79
88
  return EXIT_CODES.SUCCESS;
80
89
  }
90
+ catch (error) {
91
+ progress.stop();
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ console.error(`AgentDoctor error:\n${message}`);
94
+ return EXIT_CODES.INTERNAL_ERROR;
95
+ }
81
96
  finally {
97
+ progress.stop();
82
98
  await chat.end();
83
99
  }
84
100
  }
@@ -1255,7 +1255,8 @@ export function createProgram() {
1255
1255
  .argument("<question>", "Question about the project")
1256
1256
  .argument("[path]", "Repository path (default: current directory)")
1257
1257
  .option("--json", "Emit JSON response", false)
1258
- .action(async (question, pathArg, options) => {
1258
+ .action(async (question, pathArg, _options, command) => {
1259
+ const options = command.optsWithGlobals();
1259
1260
  process.exitCode = await runAskCommand({
1260
1261
  question,
1261
1262
  root: resolveTargetArgument(pathArg),
@@ -1,5 +1,5 @@
1
1
  import type { AgentId } from "./types/index.js";
2
- export declare const PACKAGE_VERSION = "3.0.2";
2
+ export declare const PACKAGE_VERSION = "3.0.3";
3
3
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
4
4
  /** Directories skipped during normal discovery (unless a rule needs them later). */
5
5
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "3.0.2";
1
+ export const PACKAGE_VERSION = "3.0.3";
2
2
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export const DEFAULT_IGNORE_DIRECTORIES = new Set([
@@ -128,7 +128,6 @@ export class LocalBrainStore {
128
128
  await fs.mkdir(path.join(this.root, "deltas"), { recursive: true });
129
129
  }
130
130
  async readMeta() {
131
- await this.ensureRoot();
132
131
  try {
133
132
  const raw = await fs.readFile(this.metaPath(), "utf8");
134
133
  const parsed = JSON.parse(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Engineering assurance for AI coding agents — repository intelligence, change evidence, safety controls, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {