@kolisachint/hoocode-agent 0.2.5 → 0.2.7
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/CHANGELOG.md +16 -0
- package/README.md +0 -5
- package/dist/cli/args.d.ts +0 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +0 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +2 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +0 -12
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +3 -10
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +2 -7
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -10
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/messages.d.ts +3 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +2 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/resource-loader.d.ts +0 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +25 -30
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +3 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -14
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +12 -41
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +123 -165
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +3 -8
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -5
- package/dist/init.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -9
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +12 -3
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +39 -27
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +12 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/examples/extensions/custom-compaction.ts +1 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/templates/default-config.json +1 -17
- package/templates/modes/plan/system.md +3 -3
- package/templates/modes/agent/system.md +0 -10
- /package/{templates/profiles/data/context.md → docs/profiles/data.md} +0 -0
- /package/{templates/profiles/default/context.md → docs/profiles/default.md} +0 -0
- /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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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,
|
|
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
|
|
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 .
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
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"
|
|
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
|
|
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}/
|
|
418
|
+
* `modes/{name}/system.md` content. Order: project → user → externalDirs.
|
|
479
419
|
*/
|
|
480
|
-
function
|
|
420
|
+
function resolveModeFile(name, cwd, externalDirs) {
|
|
481
421
|
const candidates = [
|
|
482
|
-
join(cwd, ".hoocode",
|
|
483
|
-
join(HOOCODE_DIR,
|
|
484
|
-
...externalDirs.map((dir) => join(dir, name,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
500
|
-
* - `./.hoocode/
|
|
501
|
-
* - `~/.hoocode/
|
|
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,
|
|
507
|
-
const layers = [];
|
|
442
|
+
export function buildSystemPrompt(mode, cwd, options) {
|
|
508
443
|
const modePaths = options?.modePaths ?? [];
|
|
509
|
-
|
|
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.
|
|
516
|
+
// C. setupMode
|
|
596
517
|
// ============================================================================
|
|
597
|
-
export function
|
|
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
|
|
606
|
-
// 4. Re-resolve active_mode
|
|
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
|
|
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.
|
|
615
|
-
// - pi.
|
|
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
|
|
618
|
-
|
|
619
|
-
//
|
|
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.
|
|
545
|
+
ctx.ui.setMode(cachedMode);
|
|
622
546
|
}
|
|
623
|
-
// Apply tool filter
|
|
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", "
|
|
562
|
+
const KNOWN_MODES = ["ask", "plan", "build", "debug"];
|
|
645
563
|
pi.registerCommand("mode", {
|
|
646
|
-
description: "Switch active mode. Usage: /mode <ask|plan|build|
|
|
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
|
-
//
|
|
709
|
-
const
|
|
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
|
-
|
|
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 .
|
|
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
|
-
|
|
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
|
-
|
|
708
|
+
setupMode(pi);
|
|
751
709
|
}
|
|
752
710
|
//# sourceMappingURL=hoo-core.js.map
|