@bahulam/code 2.6.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.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Runner — non-interactive mode for benchmarks and automation.
|
|
3
|
+
*
|
|
4
|
+
* No REPL, no spinners, no approval prompts. Auto-approves all tools.
|
|
5
|
+
* Outputs structured JSONL to stdout for machine consumption.
|
|
6
|
+
* stderr gets minimal progress (optional with --verbose).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bahulam-code --headless "Fix the bug in auth.py"
|
|
10
|
+
* bahulam-code --headless --timeout 300 --max-cost 2.00 "Refactor the login flow"
|
|
11
|
+
* bahulam-code --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { TarangStreamClient } from './stream-client.mjs';
|
|
15
|
+
import { createToolExecutor } from './tool-executor.mjs';
|
|
16
|
+
import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
|
|
17
|
+
import { persistProjectArtifacts } from './project-artifacts.mjs';
|
|
18
|
+
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
19
|
+
import { ApprovalManager } from './approval.mjs';
|
|
20
|
+
import {
|
|
21
|
+
appendVisionAnalysisToInstruction,
|
|
22
|
+
prepareImageAttachments,
|
|
23
|
+
publicAttachmentMetadata,
|
|
24
|
+
} from './attachments.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run a single instruction in headless mode.
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.instruction - the prompt to send
|
|
30
|
+
* @param {string} [opts.model] - model override
|
|
31
|
+
* @param {number} [opts.timeout] - max seconds (default: 300)
|
|
32
|
+
* @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
|
|
33
|
+
* @param {boolean} [opts.verbose] - show progress on stderr
|
|
34
|
+
*/
|
|
35
|
+
export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, vision = [] }) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
const log = (msg) => {
|
|
39
|
+
if (verbose) process.stderr.write(`[headless] ${msg}\n`);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const emit = (obj) => {
|
|
43
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ── Auth ──
|
|
47
|
+
const auth = new TarangAuth();
|
|
48
|
+
const creds = auth.loadCredentials();
|
|
49
|
+
if (!creds.token) {
|
|
50
|
+
emit({ type: 'error', error: 'Not logged in. Run: bahulam-code login' });
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Projects are registered and indexed only when the agent requests an overview.
|
|
55
|
+
const toolExecutor = createToolExecutor();
|
|
56
|
+
|
|
57
|
+
// Auto-approve everything — no prompts
|
|
58
|
+
const approval = new ApprovalManager({ autoApprove: true });
|
|
59
|
+
|
|
60
|
+
// ── Client selection ──
|
|
61
|
+
// PRD-071 Phase 2 measurement — --local forces the CLI-side LocalAgent path,
|
|
62
|
+
// bypassing the backend so we can exercise the cache_control wiring we
|
|
63
|
+
// just added to _callClaude / _callOpenRouter. Model comes from the
|
|
64
|
+
// --model flag (which overrides settings dynamically for benchmarking).
|
|
65
|
+
let client;
|
|
66
|
+
if (local) {
|
|
67
|
+
const { LocalAgent } = await import('./local-agent.mjs');
|
|
68
|
+
const localModel = model || creds.models?.local || 'anthropic/claude-sonnet-4';
|
|
69
|
+
const orKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
|
|
70
|
+
const anthKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
|
|
71
|
+
if (!orKey && !anthKey) {
|
|
72
|
+
emit({ type: 'error', error: '--local requires OPENROUTER_API_KEY or ANTHROPIC_API_KEY' });
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
client = {
|
|
76
|
+
execute: (instr, ctx) => new LocalAgent({
|
|
77
|
+
apiKey: anthKey,
|
|
78
|
+
openRouterKey: orKey,
|
|
79
|
+
model: localModel,
|
|
80
|
+
toolExecutor,
|
|
81
|
+
verbose,
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
maxTurns: 50,
|
|
84
|
+
}).execute(instr, ctx),
|
|
85
|
+
};
|
|
86
|
+
log(`Local mode: ${localModel}`);
|
|
87
|
+
} else {
|
|
88
|
+
client = new TarangStreamClient({
|
|
89
|
+
baseUrl: creds.backendUrl,
|
|
90
|
+
token: creds.token,
|
|
91
|
+
toolExecutor,
|
|
92
|
+
approvalManager: approval,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Timeout ──
|
|
97
|
+
const timeoutMs = timeout * 1000;
|
|
98
|
+
const timeoutTimer = setTimeout(() => {
|
|
99
|
+
emit({ type: 'timeout', duration_s: timeout });
|
|
100
|
+
log(`Timeout after ${timeout}s`);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
|
|
104
|
+
// ── Vision analysis preflight ──
|
|
105
|
+
if (!local) {
|
|
106
|
+
try {
|
|
107
|
+
const prepared = prepareImageAttachments(instruction, {
|
|
108
|
+
cwd: process.cwd(),
|
|
109
|
+
extraPaths: Array.isArray(vision) ? vision : [],
|
|
110
|
+
});
|
|
111
|
+
if (prepared.attachments.length) {
|
|
112
|
+
emit({
|
|
113
|
+
type: 'attachments',
|
|
114
|
+
attachments: prepared.attachments.map(publicAttachmentMetadata),
|
|
115
|
+
});
|
|
116
|
+
const analysis = await client.analyzeVision({
|
|
117
|
+
instruction: prepared.instruction,
|
|
118
|
+
attachments: prepared.attachments,
|
|
119
|
+
});
|
|
120
|
+
instruction = appendVisionAnalysisToInstruction(prepared.instruction, analysis);
|
|
121
|
+
emit({
|
|
122
|
+
type: 'vision_analysis',
|
|
123
|
+
model: analysis.model,
|
|
124
|
+
attachments: analysis.attachments || prepared.metadata,
|
|
125
|
+
summary_chars: String(analysis.summary || '').length,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
instruction = prepared.instruction || instruction;
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
emit({ type: 'error', error: err.message || String(err), code: 'vision_analysis_failed' });
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
} else if (Array.isArray(vision) && vision.length) {
|
|
135
|
+
emit({ type: 'error', error: '--vision is not supported with --local yet', code: 'vision_local_unsupported' });
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Execute ──
|
|
140
|
+
emit({ type: 'start', timestamp: Date.now(), instruction, model: model || 'default', cwd: process.cwd() });
|
|
141
|
+
|
|
142
|
+
let projectResources = toolExecutor.getProjectResources();
|
|
143
|
+
const promptRoots = promptProjectRoots(instruction);
|
|
144
|
+
if (promptRoots.length > 0) {
|
|
145
|
+
await toolExecutor.registerProjectRoots(promptRoots);
|
|
146
|
+
projectResources = toolExecutor.getProjectResources();
|
|
147
|
+
}
|
|
148
|
+
const execContext = {
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
freeswim: true,
|
|
151
|
+
project_resources: projectResources,
|
|
152
|
+
work_scope: buildWorkScope({
|
|
153
|
+
instruction,
|
|
154
|
+
cwd: process.cwd(),
|
|
155
|
+
projectResources,
|
|
156
|
+
}),
|
|
157
|
+
agent_context: toolExecutor.getAgentContext(),
|
|
158
|
+
};
|
|
159
|
+
if (model) execContext.model_override = model;
|
|
160
|
+
|
|
161
|
+
let primaryToolCount = 0;
|
|
162
|
+
let subAgentForwardedToolCount = 0;
|
|
163
|
+
let backendToolCount = null;
|
|
164
|
+
let backendPrimaryToolCount = null;
|
|
165
|
+
let backendSubAgentToolCount = null;
|
|
166
|
+
let finalContent = '';
|
|
167
|
+
let totalCost = 0;
|
|
168
|
+
let rateLimit = null;
|
|
169
|
+
|
|
170
|
+
// ── Telemetry collectors ──
|
|
171
|
+
const toolCalls = []; // { tool, call_id, duration_ms, success, internal, sub_agent }
|
|
172
|
+
const emittedToolResults = new Set();
|
|
173
|
+
const subAgents = []; // { type, model, duration_s, tool_calls, success }
|
|
174
|
+
let stagnationCount = 0;
|
|
175
|
+
let usage = {}; // { input_tokens, output_tokens, cache_read, cache_write }
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
for await (const event of client.execute(instruction, execContext)) {
|
|
179
|
+
const { type, data } = event;
|
|
180
|
+
|
|
181
|
+
if (type === 'tool_call' || type === 'tool_request') {
|
|
182
|
+
const isInternal = Boolean(data?.internal || data?.sub_agent);
|
|
183
|
+
const toolName = data?.tool || 'unknown';
|
|
184
|
+
const args = data?.args || {};
|
|
185
|
+
if (isInternal) subAgentForwardedToolCount++;
|
|
186
|
+
else primaryToolCount++;
|
|
187
|
+
toolCalls.push({
|
|
188
|
+
tool: toolName,
|
|
189
|
+
call_id: data?.call_id || data?.request_id || '',
|
|
190
|
+
success: null,
|
|
191
|
+
duration_ms: 0,
|
|
192
|
+
internal: isInternal,
|
|
193
|
+
sub_agent: data?.sub_agent || null,
|
|
194
|
+
});
|
|
195
|
+
emit({
|
|
196
|
+
type: 'tool_call',
|
|
197
|
+
tool: toolName,
|
|
198
|
+
args,
|
|
199
|
+
call_id: data?.call_id || data?.request_id || '',
|
|
200
|
+
approved: true,
|
|
201
|
+
internal: isInternal,
|
|
202
|
+
sub_agent: data?.sub_agent || null,
|
|
203
|
+
});
|
|
204
|
+
log(`Tool: ${toolName}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (type === 'tool_result' || type === 'tool_done') {
|
|
208
|
+
const success = data?.success !== false;
|
|
209
|
+
const durationMs = data?.duration_ms ?? Math.round((data?.duration_s || 0) * 1000);
|
|
210
|
+
const callId = data?.call_id || data?._callId || data?.request_id || '';
|
|
211
|
+
const isInternal = Boolean(data?.internal || data?.sub_agent);
|
|
212
|
+
// Update last tool call with result
|
|
213
|
+
const last = toolCalls.findLast(t => (callId && t.call_id === callId) || t.tool === (data?.tool || ''));
|
|
214
|
+
if (last) {
|
|
215
|
+
last.success = success;
|
|
216
|
+
last.duration_ms = durationMs;
|
|
217
|
+
if (isInternal) {
|
|
218
|
+
last.internal = true;
|
|
219
|
+
last.sub_agent = data?.sub_agent || last.sub_agent || null;
|
|
220
|
+
}
|
|
221
|
+
} else if (data?.tool) {
|
|
222
|
+
// Some backend-owned meta-tools (for example explore) emit a
|
|
223
|
+
// result/complete event without a preceding client-visible
|
|
224
|
+
// tool_call. Keep the breakdown honest without relying on a
|
|
225
|
+
// duplicate rendered call line.
|
|
226
|
+
toolCalls.push({
|
|
227
|
+
tool: data.tool,
|
|
228
|
+
call_id: callId,
|
|
229
|
+
success,
|
|
230
|
+
duration_ms: durationMs,
|
|
231
|
+
internal: isInternal,
|
|
232
|
+
sub_agent: data?.sub_agent || null,
|
|
233
|
+
});
|
|
234
|
+
if (isInternal) subAgentForwardedToolCount++;
|
|
235
|
+
else primaryToolCount++;
|
|
236
|
+
}
|
|
237
|
+
if (callId && emittedToolResults.has(callId)) continue;
|
|
238
|
+
if (callId) emittedToolResults.add(callId);
|
|
239
|
+
emit({
|
|
240
|
+
type: 'tool_result',
|
|
241
|
+
tool: data?.tool || '',
|
|
242
|
+
call_id: callId,
|
|
243
|
+
success,
|
|
244
|
+
duration_ms: durationMs,
|
|
245
|
+
internal: isInternal,
|
|
246
|
+
sub_agent: data?.sub_agent || null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (type === 'file_diff') {
|
|
251
|
+
emit({
|
|
252
|
+
type: 'file_diff',
|
|
253
|
+
tool: data?.tool || '',
|
|
254
|
+
path: data?.path || '',
|
|
255
|
+
relative_path: data?.relative_path || '',
|
|
256
|
+
lines_added: data?.lines_added || 0,
|
|
257
|
+
lines_removed: data?.lines_removed || 0,
|
|
258
|
+
truncated: !!data?.truncated,
|
|
259
|
+
hunks: data?.hunks || [],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (type === 'sub_agent_start') {
|
|
264
|
+
log(`SubAgent: ${data?.type} (${data?.model})`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (type === 'sub_agent_complete') {
|
|
268
|
+
subAgents.push({
|
|
269
|
+
type: data?.type || '',
|
|
270
|
+
model: data?.model || '',
|
|
271
|
+
duration_s: data?.duration_s || 0,
|
|
272
|
+
tool_calls: data?.tool_calls || 0,
|
|
273
|
+
success: data?.success !== false,
|
|
274
|
+
});
|
|
275
|
+
emit({ type: 'sub_agent', ...data });
|
|
276
|
+
log(`SubAgent done: ${data?.type} (${data?.tool_calls} tools, ${data?.duration_s}s)`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (type === 'plan_created' || type === 'goal_created') {
|
|
280
|
+
persistProjectArtifacts(
|
|
281
|
+
data,
|
|
282
|
+
toolExecutor.getProjectResources(),
|
|
283
|
+
log,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (type === 'stagnation' || type === 'stagnation_detected') {
|
|
288
|
+
stagnationCount++;
|
|
289
|
+
emit({ type: 'stagnation', reason: data?.reason || '', strategy: data?.recovery_strategy || '' });
|
|
290
|
+
log(`Stagnation: ${data?.reason || ''}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (type === 'content') {
|
|
294
|
+
finalContent = data?.text || '';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (type === 'content_partial') {
|
|
298
|
+
const text = data?.text || '';
|
|
299
|
+
if (text) finalContent = text;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (type === 'session_info') {
|
|
303
|
+
if (data?.rate_limit) rateLimit = data.rate_limit;
|
|
304
|
+
// Surface session_id in the JSONL so multi-turn harnesses can
|
|
305
|
+
// capture it from turn N and forward on turn N+1 (via TARANG_SESSION_ID).
|
|
306
|
+
if (data?.session_id) emit({ type: 'session_info', session_id: data.session_id });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (type === 'complete') {
|
|
310
|
+
if (data?.rate_limit) rateLimit = data.rate_limit;
|
|
311
|
+
totalCost = data?.cost || data?.total_cost || 0;
|
|
312
|
+
if (data?.usage?.total_cost) totalCost = data.usage.total_cost;
|
|
313
|
+
if (Number.isFinite(data?.tool_calls)) backendToolCount = data.tool_calls;
|
|
314
|
+
if (Number.isFinite(data?.primary_tool_calls)) backendPrimaryToolCount = data.primary_tool_calls;
|
|
315
|
+
if (Number.isFinite(data?.sub_agent_tool_calls)) backendSubAgentToolCount = data.sub_agent_tool_calls;
|
|
316
|
+
// Capture token usage
|
|
317
|
+
if (data?.usage) {
|
|
318
|
+
usage = {
|
|
319
|
+
input_tokens: data.usage.total_input_tokens || data.usage.input_tokens || 0,
|
|
320
|
+
output_tokens: data.usage.total_output_tokens || data.usage.output_tokens || 0,
|
|
321
|
+
cache_read: data.usage.cache_read_input_tokens || data.usage.cache_read || 0,
|
|
322
|
+
cache_write: data.usage.cache_creation_input_tokens || data.usage.cache_write || 0,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (type === 'error') {
|
|
328
|
+
emit({
|
|
329
|
+
type: 'error',
|
|
330
|
+
error: data?.message || 'Unknown error',
|
|
331
|
+
code: data?.code,
|
|
332
|
+
retry_after: data?.retry_after,
|
|
333
|
+
rate_limit: data?.rate_limit || null,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Cost guard ──
|
|
338
|
+
if (maxCost && totalCost > maxCost) {
|
|
339
|
+
emit({ type: 'cost_exceeded', cost_usd: totalCost, max_cost: maxCost });
|
|
340
|
+
log(`Cost exceeded: $${totalCost.toFixed(3)} > $${maxCost}`);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
emit({ type: 'error', error: err.message });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
clearTimeout(timeoutTimer);
|
|
349
|
+
|
|
350
|
+
const durationS = (Date.now() - startTime) / 1000;
|
|
351
|
+
|
|
352
|
+
// ── Tool breakdown ──
|
|
353
|
+
const countBreakdown = (items) => {
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const t of items) out[t.tool] = (out[t.tool] || 0) + 1;
|
|
356
|
+
return out;
|
|
357
|
+
};
|
|
358
|
+
const toolBreakdown = countBreakdown(toolCalls);
|
|
359
|
+
const primaryToolBreakdown = countBreakdown(toolCalls.filter(t => !t.internal));
|
|
360
|
+
const subAgentToolBreakdown = countBreakdown(toolCalls.filter(t => t.internal));
|
|
361
|
+
|
|
362
|
+
const subAgentReportedToolCount = subAgents.reduce((sum, sa) => sum + (sa.tool_calls || 0), 0);
|
|
363
|
+
const subAgentToolCount = backendSubAgentToolCount ?? Math.max(subAgentReportedToolCount, subAgentForwardedToolCount);
|
|
364
|
+
const totalToolCount = backendToolCount ?? (primaryToolCount + subAgentToolCount);
|
|
365
|
+
const primaryToolTotal = backendPrimaryToolCount ?? (
|
|
366
|
+
backendToolCount != null
|
|
367
|
+
? Math.max(0, totalToolCount - subAgentToolCount)
|
|
368
|
+
: primaryToolCount
|
|
369
|
+
);
|
|
370
|
+
if (subAgentToolCount > subAgentReportedToolCount && !backendSubAgentToolCount) {
|
|
371
|
+
subAgents.push({
|
|
372
|
+
type: 'forwarded',
|
|
373
|
+
model: '',
|
|
374
|
+
duration_s: 0,
|
|
375
|
+
tool_calls: subAgentToolCount - subAgentReportedToolCount,
|
|
376
|
+
success: true,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
emit({
|
|
381
|
+
type: 'complete',
|
|
382
|
+
tools: totalToolCount,
|
|
383
|
+
tools_primary: primaryToolTotal,
|
|
384
|
+
tools_sub_agent: subAgentToolCount,
|
|
385
|
+
tools_forwarded_sub_agent_events: subAgentForwardedToolCount,
|
|
386
|
+
tool_breakdown: toolBreakdown,
|
|
387
|
+
tool_breakdown_primary: primaryToolBreakdown,
|
|
388
|
+
tool_breakdown_sub_agent: subAgentToolBreakdown,
|
|
389
|
+
sub_agents: subAgents,
|
|
390
|
+
stagnation_triggers: stagnationCount,
|
|
391
|
+
usage,
|
|
392
|
+
duration_s: Math.round(durationS * 10) / 10,
|
|
393
|
+
cost_usd: totalCost,
|
|
394
|
+
rate_limit: rateLimit,
|
|
395
|
+
model: model || 'default',
|
|
396
|
+
content_length: finalContent.length,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// PRD-071 §1.5 — cache summary for benchmark harness. Machine-readable,
|
|
400
|
+
// one file per run. Fields match what benchmark/cache-check.sh already
|
|
401
|
+
// computes (input, cache_read, cache_write, rate) so the shell script
|
|
402
|
+
// becomes a thin reader instead of re-doing the arithmetic.
|
|
403
|
+
if (cacheReport && usage) {
|
|
404
|
+
const cacheRead = usage.cache_read || 0;
|
|
405
|
+
const cacheWrite = usage.cache_write || 0;
|
|
406
|
+
const inputT = usage.input_tokens || 0;
|
|
407
|
+
// Two conventions in the wild:
|
|
408
|
+
// OpenAI/DeepSeek: input_tokens INCLUDES cached tokens
|
|
409
|
+
// → hit_rate = cache_read / input_tokens
|
|
410
|
+
// Anthropic: input_tokens EXCLUDES cache reads AND writes
|
|
411
|
+
// → hit_rate = cache_read / (input + cache_read + cache_write)
|
|
412
|
+
// Report both. Also expose `cache_hit_rate_pct` as the "sane" number
|
|
413
|
+
// — auto-detects convention by whether cache_read > input_tokens.
|
|
414
|
+
const rateOpenAI = inputT > 0 ? Math.round((cacheRead / inputT) * 100) : 0;
|
|
415
|
+
const anthropicDenom = inputT + cacheRead + cacheWrite;
|
|
416
|
+
const rateAnthropic = anthropicDenom > 0 ? Math.round((cacheRead / anthropicDenom) * 100) : 0;
|
|
417
|
+
const rateAuto = cacheRead > inputT ? rateAnthropic : rateOpenAI;
|
|
418
|
+
const report = {
|
|
419
|
+
schema: 'kepler.cache-report/1',
|
|
420
|
+
model: model || 'default',
|
|
421
|
+
input_tokens: inputT,
|
|
422
|
+
output_tokens: usage.output_tokens || 0,
|
|
423
|
+
cache_read_tokens: cacheRead,
|
|
424
|
+
cache_write_tokens: cacheWrite,
|
|
425
|
+
cache_hit_rate_pct: rateAuto,
|
|
426
|
+
cache_hit_rate_openai_pct: rateOpenAI,
|
|
427
|
+
cache_hit_rate_anthropic_pct: rateAnthropic,
|
|
428
|
+
duration_s: Math.round(durationS * 10) / 10,
|
|
429
|
+
cost_usd: totalCost,
|
|
430
|
+
};
|
|
431
|
+
try {
|
|
432
|
+
const fs = await import('node:fs');
|
|
433
|
+
fs.writeFileSync(cacheReport, JSON.stringify(report, null, 2) + '\n');
|
|
434
|
+
log(`Cache report written: ${cacheReport}`);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
log(`Cache report write failed: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
log(`Done: ${totalToolCount} tools (${primaryToolTotal} primary, ${subAgentToolCount} sub-agent), ${durationS.toFixed(1)}s, $${totalCost.toFixed(3)}`);
|
|
441
|
+
|
|
442
|
+
// Write final content to stderr so it's human-readable (stdout is JSONL)
|
|
443
|
+
if (verbose && finalContent) {
|
|
444
|
+
process.stderr.write(`\n--- Response ---\n${finalContent.slice(0, 2000)}\n`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Manager — T22: PreToolUse/PostToolUse hooks from config.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { projectHooksPath, globalHooksPath } from './paths.mjs';
|
|
9
|
+
|
|
10
|
+
export class HooksManager {
|
|
11
|
+
constructor(projectDir = process.cwd()) {
|
|
12
|
+
this.projectHooksPath = projectHooksPath(projectDir);
|
|
13
|
+
this.globalHooksPath = globalHooksPath();
|
|
14
|
+
this.hooks = this._loadHooks();
|
|
15
|
+
this.firedHooks = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_loadHooks() {
|
|
19
|
+
const hooks = { PreToolUse: [], PostToolUse: [] };
|
|
20
|
+
|
|
21
|
+
// Global hooks (lower priority)
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(this.globalHooksPath)) {
|
|
24
|
+
const global = JSON.parse(fs.readFileSync(this.globalHooksPath, 'utf-8'));
|
|
25
|
+
if (global.PreToolUse) hooks.PreToolUse.push(...global.PreToolUse);
|
|
26
|
+
if (global.PostToolUse) hooks.PostToolUse.push(...global.PostToolUse);
|
|
27
|
+
}
|
|
28
|
+
} catch { /* skip corrupt */ }
|
|
29
|
+
|
|
30
|
+
// Project hooks (higher priority, appended after global)
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(this.projectHooksPath)) {
|
|
33
|
+
const project = JSON.parse(fs.readFileSync(this.projectHooksPath, 'utf-8'));
|
|
34
|
+
if (project.PreToolUse) hooks.PreToolUse.push(...project.PreToolUse);
|
|
35
|
+
if (project.PostToolUse) hooks.PostToolUse.push(...project.PostToolUse);
|
|
36
|
+
}
|
|
37
|
+
} catch { /* skip corrupt */ }
|
|
38
|
+
|
|
39
|
+
return hooks;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Run PreToolUse hooks. Returns { allowed, message }. */
|
|
43
|
+
runPreToolUse(toolName, toolInput) {
|
|
44
|
+
return this._runHooks('PreToolUse', toolName, toolInput);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Run PostToolUse hooks. Returns { success }. */
|
|
48
|
+
runPostToolUse(toolName, toolInput, result) {
|
|
49
|
+
return this._runHooks('PostToolUse', toolName, toolInput, result);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_runHooks(event, toolName, toolInput, result = null) {
|
|
53
|
+
const hooks = (this.hooks[event] || []).filter(h => !h.tool || h.tool === toolName);
|
|
54
|
+
for (const hook of hooks) {
|
|
55
|
+
try {
|
|
56
|
+
const env = {
|
|
57
|
+
...process.env,
|
|
58
|
+
HOOK_EVENT: event,
|
|
59
|
+
TOOL_NAME: toolName,
|
|
60
|
+
TOOL_INPUT: JSON.stringify(toolInput),
|
|
61
|
+
FILE_PATH: toolInput?.path || toolInput?.file_path || '',
|
|
62
|
+
};
|
|
63
|
+
if (result) env.TOOL_RESULT = JSON.stringify(result);
|
|
64
|
+
|
|
65
|
+
execSync(hook.command, { env, stdio: 'pipe', timeout: 10_000 });
|
|
66
|
+
this.firedHooks.push({ event, tool: toolName, command: hook.command, success: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
this.firedHooks.push({ event, tool: toolName, command: hook.command, success: false });
|
|
69
|
+
if (event === 'PreToolUse') {
|
|
70
|
+
return { allowed: false, message: `Hook blocked ${toolName}: ${err.message}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { allowed: true, success: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** List configured hooks. */
|
|
78
|
+
listHooks() { return this.hooks; }
|
|
79
|
+
|
|
80
|
+
/** List hooks that fired this session. */
|
|
81
|
+
getFiredHooks() { return this.firedHooks; }
|
|
82
|
+
|
|
83
|
+
/** Check if any hooks are configured. */
|
|
84
|
+
hasHooks() {
|
|
85
|
+
return (this.hooks.PreToolUse.length + this.hooks.PostToolUse.length) > 0;
|
|
86
|
+
}
|
|
87
|
+
}
|