@kolisachint/hoocode-agent 0.2.4 → 0.2.6

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 (81) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +0 -5
  3. package/dist/cli/args.d.ts +0 -2
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +0 -10
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +10 -2
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  11. package/dist/core/compaction/branch-summarization.js +1 -1
  12. package/dist/core/compaction/branch-summarization.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts +2 -0
  14. package/dist/core/compaction/compaction.d.ts.map +1 -1
  15. package/dist/core/compaction/compaction.js +11 -1
  16. package/dist/core/compaction/compaction.js.map +1 -1
  17. package/dist/core/extensions/loader.d.ts.map +1 -1
  18. package/dist/core/extensions/loader.js +0 -12
  19. package/dist/core/extensions/loader.js.map +1 -1
  20. package/dist/core/extensions/runner.d.ts.map +1 -1
  21. package/dist/core/extensions/runner.js +1 -1
  22. package/dist/core/extensions/runner.js.map +1 -1
  23. package/dist/core/extensions/types.d.ts +3 -10
  24. package/dist/core/extensions/types.d.ts.map +1 -1
  25. package/dist/core/extensions/types.js.map +1 -1
  26. package/dist/core/footer-data-provider.d.ts +2 -7
  27. package/dist/core/footer-data-provider.d.ts.map +1 -1
  28. package/dist/core/footer-data-provider.js +1 -10
  29. package/dist/core/footer-data-provider.js.map +1 -1
  30. package/dist/core/messages.d.ts +3 -1
  31. package/dist/core/messages.d.ts.map +1 -1
  32. package/dist/core/messages.js +2 -1
  33. package/dist/core/messages.js.map +1 -1
  34. package/dist/core/resource-loader.d.ts +0 -2
  35. package/dist/core/resource-loader.d.ts.map +1 -1
  36. package/dist/core/resource-loader.js +25 -30
  37. package/dist/core/resource-loader.js.map +1 -1
  38. package/dist/core/session-manager.d.ts +3 -1
  39. package/dist/core/session-manager.d.ts.map +1 -1
  40. package/dist/core/session-manager.js +3 -2
  41. package/dist/core/session-manager.js.map +1 -1
  42. package/dist/core/system-prompt.d.ts.map +1 -1
  43. package/dist/core/system-prompt.js +1 -14
  44. package/dist/core/system-prompt.js.map +1 -1
  45. package/dist/extensions/core/hoo-core.d.ts +12 -41
  46. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  47. package/dist/extensions/core/hoo-core.js +123 -165
  48. package/dist/extensions/core/hoo-core.js.map +1 -1
  49. package/dist/init-templates.generated.d.ts.map +1 -1
  50. package/dist/init-templates.generated.js +3 -8
  51. package/dist/init-templates.generated.js.map +1 -1
  52. package/dist/init.d.ts.map +1 -1
  53. package/dist/init.js +2 -7
  54. package/dist/init.js.map +1 -1
  55. package/dist/main.d.ts.map +1 -1
  56. package/dist/main.js +4 -9
  57. package/dist/main.js.map +1 -1
  58. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/compaction-summary-message.js +12 -3
  60. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  61. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/footer.js +39 -27
  63. package/dist/modes/interactive/components/footer.js.map +1 -1
  64. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  65. package/dist/modes/interactive/interactive-mode.js +12 -8
  66. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  67. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  68. package/dist/modes/rpc/rpc-mode.js +2 -2
  69. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  70. package/examples/extensions/custom-compaction.ts +1 -0
  71. package/examples/extensions/custom-provider-anthropic/package.json +20 -20
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +17 -17
  73. package/examples/extensions/sandbox/package.json +1 -1
  74. package/examples/extensions/with-deps/package.json +23 -23
  75. package/package.json +4 -4
  76. package/templates/default-config.json +1 -17
  77. package/templates/modes/plan/system.md +3 -3
  78. package/templates/modes/agent/system.md +0 -10
  79. /package/{templates/profiles/data/context.md → docs/profiles/data.md} +0 -0
  80. /package/{templates/profiles/default/context.md → docs/profiles/default.md} +0 -0
  81. /package/{templates/profiles/devops/context.md → docs/profiles/devops.md} +0 -0
@@ -6,26 +6,24 @@
6
6
  * choices back to the global config
7
7
  * B. MCP Server Loader — discovers ~/.hoocode/mcp-servers and ./.hoocode/mcp-servers JSON
8
8
  * configs, connects via JSON-RPC 2.0, registers server tools
9
- * C. Mode + Profile — resolves active mode (ask/plan/build/agent/debug) and profile
10
- * (default/data/devops/…), merges system prompt from three template
11
- * layers, filters active tools, and exposes /mode, /profile,
12
- * /plan, and /approve commands
9
+ * C. Mode — resolves active mode (ask/plan/build/debug), loads the mode's
10
+ * system prompt, filters active tools, and exposes /mode, /plan,
11
+ * and /approve commands
13
12
  *
14
13
  * Config merge order (lowest → highest priority):
15
14
  * 1. ~/.hoocode/agent/hoo-config.json (global defaults)
16
15
  * 2. ./.hoocode/config.json (project overrides — scalars win; arrays union)
17
- * 3. profile_detectors from project prepend global list (project markers checked first)
18
16
  */
19
17
  import { spawn } from "node:child_process";
20
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
19
  import { readdir } from "node:fs/promises";
22
- import { join } from "node:path";
20
+ import { join, relative } from "node:path";
23
21
  import { createInterface } from "node:readline";
24
22
  import { Type } from "typebox";
25
23
  import { getHooCodeDir } from "../../config.js";
26
24
  import { isToolCallEventType } from "../../core/extensions/types.js";
27
25
  // ============================================================================
28
- // Fallback defaults for mode and profile prompts
26
+ // Fallback defaults for mode prompts
29
27
  // ============================================================================
30
28
  const MODE_DEFAULTS = {
31
29
  ask: `You are in ASK mode — read-only Q&A.
@@ -36,7 +34,7 @@ Cite specific file paths and line numbers in your answers.`,
36
34
  plan: `You are in PLAN mode — exploration and planning.
37
35
  Explore the codebase thoroughly. Understand the current structure.
38
36
  Draft a complete plan with sections: Goal, Files to modify, New files, Tests, Verification.
39
- Write the plan to .hoocode/plan.md.
37
+ Write the plan to {{PLAN_PATH}}.
40
38
  When the plan is complete, tell the user to run /approve to execute it.`,
41
39
  build: `You are in BUILD mode — careful implementation.
42
40
  Read files before editing them. Show diffs before non-trivial changes.
@@ -44,11 +42,6 @@ Ask for confirmation before destructive operations (delete, reformat).
44
42
  Run tests after every logical unit of work.
45
43
  Prefer the smallest change that achieves the goal.
46
44
  Follow existing code patterns and conventions.`,
47
- agent: `You are in AGENT mode — autonomous multi-step work.
48
- You have full access to read, bash, edit, and write tools.
49
- Work through problems step by step. Report progress every few steps.
50
- Stop and ask if you hit genuine ambiguity or need a decision.
51
- Output a summary of what was done when you finish.`,
52
45
  debug: `You are in DEBUG mode — root cause analysis.
53
46
  Gather evidence: read files, check logs, reproduce the issue.
54
47
  Trace the call path from entry to failure point.
@@ -56,25 +49,22 @@ State the root cause in one sentence.
56
49
  Describe the fix precisely but do NOT apply it.
57
50
  To fix, switch to /mode build.`,
58
51
  };
59
- const PROFILE_DEFAULTS = {
60
- data: `**Profile: Data Engineering**
61
- - Dry-run before mutating SQL statements.
62
- - No SELECT * on large tables — always specify columns.
63
- - Inspect table schemas before writing queries.
64
- - Validate join keys and cardinality.
65
- - Prefer incremental processing over full refreshes.`,
66
- devops: `**Profile: DevOps / Infrastructure**
67
- - Never run terraform apply or kubectl delete without showing the plan first.
68
- - Prefer declarative configuration over imperative commands.
69
- - Never hardcode secrets — use environment variables or secret managers.
70
- - Every change needs a rollback strategy.
71
- - Check existing resources before creating new ones.`,
72
- };
73
52
  // ============================================================================
74
53
  // Shared paths
75
54
  // ============================================================================
76
55
  const HOOCODE_DIR = getHooCodeDir();
77
56
  const GLOBAL_CONFIG_PATH = join(HOOCODE_DIR, "agent", "hoo-config.json");
57
+ /**
58
+ * Per-session plan file path. Keying on sessionId lets concurrent or resumed
59
+ * plan sessions keep distinct plans instead of clobbering each other.
60
+ */
61
+ function getPlanPath(cwd, sessionId) {
62
+ return join(cwd, ".hoocode", "plans", `${sessionId}.md`);
63
+ }
64
+ /** Legacy single-file plan location, retained as a read-only fallback for /approve. */
65
+ function getLegacyPlanPath(cwd) {
66
+ return join(cwd, ".hoocode", "plan.md");
67
+ }
78
68
  // ============================================================================
79
69
  // Config I/O and merging
80
70
  // ============================================================================
@@ -96,19 +86,16 @@ function writeConfig(config) {
96
86
  * Deep-merges a project-local config on top of the global config.
97
87
  *
98
88
  * Merge rules:
99
- * - Scalars (active_mode, active_profile): project wins if set
89
+ * - active_mode: project wins if set
100
90
  * - modes[x].auto_allow: union of global + project arrays
101
91
  * - modes[x].allowed_write_paths: union of global + project arrays
102
92
  * - modes[x].enabled_tools: project wins if set, else falls back to global
103
- * - profiles[x].enabled_tools: project wins if set, else falls back to global
104
- * - profile_detectors: project list is prepended so project markers are checked first
93
+ * - mode_paths: project list is prepended so project paths are searched first
105
94
  */
106
95
  export function mergeConfigs(global, project) {
107
96
  const merged = { ...global };
108
97
  if (project.active_mode !== undefined)
109
98
  merged.active_mode = project.active_mode;
110
- if (project.active_profile !== undefined)
111
- merged.active_profile = project.active_profile;
112
99
  if (project.modes) {
113
100
  merged.modes = { ...(global.modes ?? {}) };
114
101
  for (const [mode, projectCfg] of Object.entries(project.modes)) {
@@ -125,26 +112,10 @@ export function mergeConfigs(global, project) {
125
112
  };
126
113
  }
127
114
  }
128
- if (project.profiles) {
129
- merged.profiles = { ...(global.profiles ?? {}) };
130
- for (const [profile, projectCfg] of Object.entries(project.profiles)) {
131
- merged.profiles[profile] = {
132
- ...(global.profiles?.[profile] ?? {}),
133
- ...projectCfg,
134
- };
135
- }
136
- }
137
- if (project.profile_detectors) {
138
- // Project detectors are prepended: project-specific markers are checked first
139
- merged.profile_detectors = [...project.profile_detectors, ...(global.profile_detectors ?? [])];
140
- }
141
115
  if (project.mode_paths || global.mode_paths) {
142
116
  // Project paths first so they're searched before global paths
143
117
  merged.mode_paths = dedupePaths([...(project.mode_paths ?? []), ...(global.mode_paths ?? [])]);
144
118
  }
145
- if (project.profile_paths || global.profile_paths) {
146
- merged.profile_paths = dedupePaths([...(project.profile_paths ?? []), ...(global.profile_paths ?? [])]);
147
- }
148
119
  return merged;
149
120
  }
150
121
  function dedupePaths(paths) {
@@ -240,7 +211,7 @@ export function setupPermissionGate(pi) {
240
211
  block: true,
241
212
  reason: `Mode "${mode}" only allows writes to: ${modeCfg.allowed_write_paths.join(", ")}. ` +
242
213
  `Attempted to ${event.toolName}: ${filePath}. ` +
243
- `Switch to "/mode build" or "/mode agent" to modify source files.`,
214
+ `Switch to "/mode build" to modify source files.`,
244
215
  };
245
216
  }
246
217
  }
@@ -428,40 +399,9 @@ export function setupMcpLoader(pi) {
428
399
  });
429
400
  }
430
401
  // ============================================================================
431
- // C. Mode + Profile System
402
+ // C. Mode System
432
403
  // ============================================================================
433
404
  const DEFAULT_MODE = "build";
434
- const DEFAULT_PROFILE = "default";
435
- /**
436
- * Returns true if `marker` matches something in `cwd`.
437
- * Plain markers use existsSync. Glob markers (containing `*`) scan the
438
- * immediate directory entries — only one level, no recursion needed for
439
- * common cases like `*.sql` or `k8s/`.
440
- */
441
- function markerExists(cwd, marker) {
442
- if (!marker.includes("*"))
443
- return existsSync(join(cwd, marker));
444
- const suffix = marker.replace(/^\*/, "");
445
- try {
446
- return readdirSync(cwd).some((entry) => entry.endsWith(suffix));
447
- }
448
- catch {
449
- return false;
450
- }
451
- }
452
- /**
453
- * Resolves which profile should be active.
454
- * Priority: config override → file-marker detection → "default"
455
- */
456
- export function resolveProfile(config, cwd) {
457
- if (config.active_profile)
458
- return config.active_profile;
459
- for (const detector of config.profile_detectors ?? []) {
460
- if (markerExists(cwd, detector.marker))
461
- return detector.profile;
462
- }
463
- return DEFAULT_PROFILE;
464
- }
465
405
  function tryReadFile(path) {
466
406
  if (!existsSync(path))
467
407
  return undefined;
@@ -475,13 +415,13 @@ function tryReadFile(path) {
475
415
  }
476
416
  /**
477
417
  * Walks search dirs in precedence order and returns the first existing
478
- * `{name}/{filename}` content. Order: project → user → externalDirs.
418
+ * `modes/{name}/system.md` content. Order: project → user → externalDirs.
479
419
  */
480
- function resolveResourceFile(name, filename, subdir, cwd, externalDirs) {
420
+ function resolveModeFile(name, cwd, externalDirs) {
481
421
  const candidates = [
482
- join(cwd, ".hoocode", subdir, name, filename),
483
- join(HOOCODE_DIR, subdir, name, filename),
484
- ...externalDirs.map((dir) => join(dir, name, filename)),
422
+ join(cwd, ".hoocode", "modes", name, "system.md"),
423
+ join(HOOCODE_DIR, "modes", name, "system.md"),
424
+ ...externalDirs.map((dir) => join(dir, name, "system.md")),
485
425
  ];
486
426
  for (const candidate of candidates) {
487
427
  const content = tryReadFile(candidate);
@@ -491,36 +431,17 @@ function resolveResourceFile(name, filename, subdir, cwd, externalDirs) {
491
431
  return undefined;
492
432
  }
493
433
  /**
494
- * Merges the system prompt from up to three layers (lowest → highest priority):
495
- * 1. {project|user|external}/modes/{mode}/system.md (mode behaviour)
496
- * 2. {project|user|external}/profiles/{profile}/context.md (domain context; skipped for "default")
497
- * 3. ./.hoocode/agents.md (project-local override; appended last)
434
+ * Returns the system prompt for the active mode.
498
435
  *
499
- * For each of layers 1 and 2 the search order is:
500
- * - `./.hoocode/{modes,profiles}/{name}/...`
501
- * - `~/.hoocode/{modes,profiles}/{name}/...`
436
+ * Search order (first hit wins):
437
+ * - `./.hoocode/modes/{mode}/system.md`
438
+ * - `~/.hoocode/modes/{mode}/system.md`
502
439
  * - each of `externalDirs` in declared order (config + CLI + extension contributions)
503
- *
504
- * Each present layer is joined with a `---` separator.
440
+ * - built-in MODE_DEFAULTS for the four known modes
505
441
  */
506
- export function buildSystemPrompt(mode, profile, cwd, options) {
507
- const layers = [];
442
+ export function buildSystemPrompt(mode, cwd, options) {
508
443
  const modePaths = options?.modePaths ?? [];
509
- const profilePaths = options?.profilePaths ?? [];
510
- const modePrompt = resolveResourceFile(mode, "system.md", "modes", cwd, modePaths) ?? MODE_DEFAULTS[mode];
511
- if (modePrompt)
512
- layers.push(modePrompt);
513
- if (profile !== DEFAULT_PROFILE) {
514
- const profileContext = resolveResourceFile(profile, "context.md", "profiles", cwd, profilePaths) ?? PROFILE_DEFAULTS[profile];
515
- if (profileContext)
516
- layers.push(profileContext);
517
- }
518
- // Layer 3: project-local agents.md — appended after mode + profile so it can
519
- // extend or override them for this specific repo
520
- const projectOverride = tryReadFile(join(cwd, ".hoocode", "agents.md"));
521
- if (projectOverride)
522
- layers.push(projectOverride);
523
- return layers.length > 0 ? layers.join("\n\n---\n\n") : undefined;
444
+ return resolveModeFile(mode, cwd, modePaths) ?? MODE_DEFAULTS[mode];
524
445
  }
525
446
  /**
526
447
  * Parses `.hoocode/plan.md` into named sections.
@@ -592,58 +513,55 @@ export function buildApproveMessage(sections) {
592
513
  return `Execute this plan step by step. Complete each step fully before moving to the next.\n\n${steps.join("\n\n")}`;
593
514
  }
594
515
  // ============================================================================
595
- // C. setupModeAndProfile
516
+ // C. setupMode
596
517
  // ============================================================================
597
- export function setupModeAndProfile(pi) {
518
+ export function setupMode(pi) {
598
519
  let cachedMode = DEFAULT_MODE;
599
- let cachedProfile = DEFAULT_PROFILE;
600
520
  let cachedSystemPrompt;
521
+ let cachedPlanPath;
601
522
  // ── session_start ─────────────────────────────────────────────────────────
602
523
  // Config resolution order:
603
524
  // 1. Read global config (~/.hoocode/agent/hoo-config.json)
604
525
  // 2. Read project config (./.hoocode/config.json) if present
605
- // 3. Merge — project scalars win; arrays are unioned; project detectors prepend
606
- // 4. Re-resolve active_mode and active_profile from the merged result
526
+ // 3. Merge — project scalars win; arrays are unioned
527
+ // 4. Re-resolve active_mode from the merged result
607
528
  pi.on("session_start", (_event, ctx) => {
608
529
  // Steps 1–3: merge global + project configs
609
530
  const config = readMergedConfig(ctx.cwd);
610
- // Step 4: resolve mode and profile from the merged config
531
+ // Step 4: resolve mode from the merged config
611
532
  cachedMode = config.active_mode ?? DEFAULT_MODE;
612
- cachedProfile = resolveProfile(config, ctx.cwd);
613
533
  // External search dirs come from two channels:
614
- // - HooConfig.{mode,profile}_paths (config-declared)
615
- // - pi.add{Mode,Profile}SearchPath (CLI flags + extension contributions)
534
+ // - HooConfig.mode_paths (config-declared)
535
+ // - pi.addModeSearchPath (CLI flags + extension contributions)
616
536
  const modePaths = mergeSearchPaths(config.mode_paths, pi.getModeSearchPaths());
617
- const profilePaths = mergeSearchPaths(config.profile_paths, pi.getProfileSearchPaths());
618
- cachedSystemPrompt = buildSystemPrompt(cachedMode, cachedProfile, ctx.cwd, { modePaths, profilePaths });
619
- // Update footer with active mode/profile
537
+ const rawSystemPrompt = buildSystemPrompt(cachedMode, ctx.cwd, { modePaths });
538
+ // Per-session plan path so concurrent sessions don't overwrite each other.
539
+ // The `{{PLAN_PATH}}` token in plan-mode templates is substituted here.
540
+ cachedPlanPath = getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());
541
+ const relPlanPath = relative(ctx.cwd, cachedPlanPath) || cachedPlanPath;
542
+ cachedSystemPrompt = rawSystemPrompt?.replace(/\{\{PLAN_PATH\}\}/g, relPlanPath);
543
+ // Update footer with active mode
620
544
  if (ctx.hasUI) {
621
- ctx.ui.setModeProfile(cachedMode, cachedProfile);
545
+ ctx.ui.setMode(cachedMode);
622
546
  }
623
- // Apply tool filter: mode enabled_tools takes priority, then profile
547
+ // Apply tool filter from mode enabled_tools
624
548
  const modeCfg = config.modes?.[cachedMode];
625
- const profileCfg = config.profiles?.[cachedProfile];
626
549
  if (modeCfg?.enabled_tools && modeCfg.enabled_tools.length > 0) {
627
550
  pi.setActiveTools(modeCfg.enabled_tools);
628
551
  }
629
- else if (profileCfg?.enabled_tools && profileCfg.enabled_tools.length > 0) {
630
- pi.setActiveTools(profileCfg.enabled_tools);
631
- }
632
552
  });
633
553
  // ── before_agent_start ────────────────────────────────────────────────────
634
554
  pi.on("before_agent_start", (event) => {
635
555
  if (!cachedSystemPrompt)
636
556
  return;
637
557
  return {
638
- systemPrompt: `${event.systemPrompt}\n\n` +
639
- `<!-- hoo-core: mode=${cachedMode} profile=${cachedProfile} -->\n` +
640
- cachedSystemPrompt,
558
+ systemPrompt: `${event.systemPrompt}\n\n<!-- hoo-core: mode=${cachedMode} -->\n${cachedSystemPrompt}`,
641
559
  };
642
560
  });
643
561
  // ── /mode command ─────────────────────────────────────────────────────────
644
- const KNOWN_MODES = ["ask", "plan", "build", "agent", "debug"];
562
+ const KNOWN_MODES = ["ask", "plan", "build", "debug"];
645
563
  pi.registerCommand("mode", {
646
- description: "Switch active mode. Usage: /mode <ask|plan|build|agent|debug>",
564
+ description: "Switch active mode. Usage: /mode <ask|plan|build|debug>",
647
565
  getArgumentCompletions: (prefix) => KNOWN_MODES.filter((m) => m.startsWith(prefix)).map((m) => ({ value: m, label: m })),
648
566
  handler: async (args, ctx) => {
649
567
  const name = args.trim();
@@ -658,29 +576,6 @@ export function setupModeAndProfile(pi) {
658
576
  await ctx.reload();
659
577
  },
660
578
  });
661
- // ── /profile command ──────────────────────────────────────────────────────
662
- pi.registerCommand("profile", {
663
- description: "Switch active profile. Usage: /profile <name>",
664
- getArgumentCompletions: (prefix) => {
665
- // Show profiles from the merged config so project-local profiles appear
666
- const config = readMergedConfig(".");
667
- const names = Object.keys(config.profiles ?? {});
668
- const suggestions = [DEFAULT_PROFILE, ...names.filter((n) => n !== DEFAULT_PROFILE)];
669
- return suggestions.filter((n) => n.startsWith(prefix)).map((n) => ({ value: n, label: n }));
670
- },
671
- handler: async (args, ctx) => {
672
- const name = args.trim();
673
- if (!name) {
674
- ctx.ui.notify(`Active profile: ${cachedProfile}`, "info");
675
- return;
676
- }
677
- const config = readConfig();
678
- config.active_profile = name === DEFAULT_PROFILE ? undefined : name;
679
- writeConfig(config);
680
- ctx.ui.notify(`Profile set to "${name}" — reloading…`, "info");
681
- await ctx.reload();
682
- },
683
- });
684
579
  // ── /plan command (shorthand for /mode plan) ──────────────────────────────
685
580
  pi.registerCommand("plan", {
686
581
  description: "Switch to plan mode. Shorthand for /mode plan.",
@@ -705,19 +600,23 @@ export function setupModeAndProfile(pi) {
705
600
  ctx.ui.notify(`/approve is only available in plan mode (current mode: "${cachedMode}")`, "warning");
706
601
  return;
707
602
  }
708
- // Read ./.hoocode/plan.md written by the agent during plan mode
709
- const planPath = join(ctx.cwd, ".hoocode", "plan.md");
603
+ // Prefer the per-session plan file, fall back to the legacy single file.
604
+ const sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());
605
+ const candidatePaths = [sessionPlanPath, getLegacyPlanPath(ctx.cwd)];
710
606
  let approveMessage;
711
- if (existsSync(planPath)) {
607
+ for (const planPath of candidatePaths) {
608
+ if (!existsSync(planPath))
609
+ continue;
712
610
  try {
713
611
  const raw = readFileSync(planPath, "utf8").trim();
714
612
  if (raw) {
715
613
  const sections = parsePlanSections(raw);
716
614
  approveMessage = buildApproveMessage(sections);
615
+ break;
717
616
  }
718
617
  }
719
618
  catch {
720
- ctx.ui.notify(`Could not read .hoocode/plan.md`, "error");
619
+ ctx.ui.notify(`Could not read ${relative(ctx.cwd, planPath) || planPath}`, "error");
721
620
  return;
722
621
  }
723
622
  }
@@ -735,11 +634,70 @@ export function setupModeAndProfile(pi) {
735
634
  });
736
635
  }
737
636
  else {
738
- ctx.ui.notify(`Switched to build mode. No .hoocode/plan.md found — describe what to build.`, "info");
637
+ const relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;
638
+ ctx.ui.notify(`Switched to build mode. No ${relPlan} found — describe what to build.`, "info");
739
639
  await ctx.reload();
740
640
  }
741
641
  },
742
642
  });
643
+ // ── /cost command ─────────────────────────────────────────────────────────
644
+ // Walks every assistant message in the current session and sums tokens + cost,
645
+ // then prints a session total followed by a per-model breakdown.
646
+ // Per-tool attribution is intentionally not shown — tokens aren't tracked
647
+ // per-tool, and any heuristic would be misleading.
648
+ pi.registerCommand("cost", {
649
+ description: "Show session token and cost totals, broken down by model.",
650
+ getArgumentCompletions: () => [],
651
+ handler: async (_args, ctx) => {
652
+ const empty = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
653
+ const total = empty();
654
+ const perModel = new Map();
655
+ let assistantTurns = 0;
656
+ for (const entry of ctx.sessionManager.getEntries()) {
657
+ if (entry.type !== "message" || entry.message.role !== "assistant")
658
+ continue;
659
+ const u = entry.message.usage;
660
+ if (!u)
661
+ continue;
662
+ assistantTurns++;
663
+ total.input += u.input;
664
+ total.output += u.output;
665
+ total.cacheRead += u.cacheRead;
666
+ total.cacheWrite += u.cacheWrite;
667
+ total.cost += u.cost.total;
668
+ const key = `${entry.message.provider}/${entry.message.model}`;
669
+ const t = perModel.get(key) ?? empty();
670
+ t.input += u.input;
671
+ t.output += u.output;
672
+ t.cacheRead += u.cacheRead;
673
+ t.cacheWrite += u.cacheWrite;
674
+ t.cost += u.cost.total;
675
+ perModel.set(key, t);
676
+ }
677
+ if (assistantTurns === 0) {
678
+ ctx.ui.notify("No assistant turns yet — nothing to cost.", "info");
679
+ return;
680
+ }
681
+ const fmt = (n) => n.toLocaleString();
682
+ const fmtCost = (n) => `$${n.toFixed(4)}`;
683
+ const lines = [];
684
+ lines.push(`Session totals (${assistantTurns} assistant turn${assistantTurns === 1 ? "" : "s"})`);
685
+ lines.push(` Input ${fmt(total.input)}`);
686
+ lines.push(` Output ${fmt(total.output)}`);
687
+ lines.push(` Cache read ${fmt(total.cacheRead)}`);
688
+ lines.push(` Cache write ${fmt(total.cacheWrite)}`);
689
+ lines.push(` Cost ${fmtCost(total.cost)}`);
690
+ if (perModel.size > 1) {
691
+ lines.push("");
692
+ lines.push("By model:");
693
+ const sorted = [...perModel.entries()].sort((a, b) => b[1].cost - a[1].cost);
694
+ for (const [key, t] of sorted) {
695
+ lines.push(` ${key}: ${fmt(t.input)} in / ${fmt(t.output)} out ${fmtCost(t.cost)}`);
696
+ }
697
+ }
698
+ ctx.ui.notify(lines.join("\n"), "info");
699
+ },
700
+ });
743
701
  }
744
702
  // ============================================================================
745
703
  // Extension entry point
@@ -747,6 +705,6 @@ export function setupModeAndProfile(pi) {
747
705
  export default function hooCore(pi) {
748
706
  setupPermissionGate(pi);
749
707
  setupMcpLoader(pi);
750
- setupModeAndProfile(pi);
708
+ setupMode(pi);
751
709
  }
752
710
  //# sourceMappingURL=hoo-core.js.map