@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,3832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kepler REPL — Full Claude-like terminal UX.
|
|
3
|
+
*
|
|
4
|
+
* Pure ANSI. No React. No Ink. No flickering.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Persistent status bar (model, cost, context, elapsed)
|
|
8
|
+
* - Streaming content with live partial updates
|
|
9
|
+
* - Tool execution display (transparent, collapsible)
|
|
10
|
+
* - File diff display with +/- highlighting
|
|
11
|
+
* - Phase/worker progress indicators
|
|
12
|
+
* - Built-in agents (explore, review, architect)
|
|
13
|
+
* - Permission prompts (Y/n/a/t)
|
|
14
|
+
* - Input history & Tab autocomplete
|
|
15
|
+
* - Safety guardrails on all tool execution
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as readline from 'node:readline';
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { execSync as _execSync } from 'node:child_process';
|
|
22
|
+
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
23
|
+
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
24
|
+
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
25
|
+
import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
|
|
26
|
+
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
27
|
+
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
28
|
+
import { buildWorkScope, promptProjectRoots } from '../core/work-scope.mjs';
|
|
29
|
+
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
30
|
+
import { HookRunner } from '../config/hook-runner.mjs';
|
|
31
|
+
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
32
|
+
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
33
|
+
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
34
|
+
import {
|
|
35
|
+
getVerbosity,
|
|
36
|
+
setVerbosity,
|
|
37
|
+
label as verbosityLabel,
|
|
38
|
+
MODES as V_MODES,
|
|
39
|
+
} from '../state/verbosity.mjs';
|
|
40
|
+
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
41
|
+
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
42
|
+
import { ApprovalManager } from '../core/approval.mjs';
|
|
43
|
+
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
44
|
+
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
45
|
+
import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
|
|
46
|
+
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
47
|
+
import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
48
|
+
import { SessionManager } from '../core/session-manager.mjs';
|
|
49
|
+
import { parseArgs } from '../config/cli-args.mjs';
|
|
50
|
+
import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
|
|
51
|
+
import { loadProjectContext } from '../core/project-context-loader.mjs';
|
|
52
|
+
import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
53
|
+
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
54
|
+
import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
|
|
55
|
+
import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
|
|
56
|
+
import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
|
|
57
|
+
import {
|
|
58
|
+
appendVisionAnalysisToInstruction,
|
|
59
|
+
attachmentSummaryLine,
|
|
60
|
+
prepareImageAttachments,
|
|
61
|
+
publicAttachmentMetadata,
|
|
62
|
+
resolveAttachmentPath,
|
|
63
|
+
writeClipboardImageToTemp,
|
|
64
|
+
} from '../core/attachments.mjs';
|
|
65
|
+
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
66
|
+
import { exploreCategory, exploreCollapseEnabled, isExploreTool } from './repl-explore.mjs';
|
|
67
|
+
import { session, orbitRef, sessionMgrRef, runtime } from './repl-state.mjs';
|
|
68
|
+
import { safeCwd } from './repl-utils.mjs';
|
|
69
|
+
import {
|
|
70
|
+
appendContent,
|
|
71
|
+
clearPendingHead,
|
|
72
|
+
clippedThinking,
|
|
73
|
+
expandIndex,
|
|
74
|
+
expandLast,
|
|
75
|
+
flushContent,
|
|
76
|
+
flushExploreRun,
|
|
77
|
+
flushPendingHead,
|
|
78
|
+
isInlineOutcomeTool,
|
|
79
|
+
renderBlockBoundary,
|
|
80
|
+
renderExploreRun,
|
|
81
|
+
renderStagnation,
|
|
82
|
+
renderToolCall,
|
|
83
|
+
renderToolResult,
|
|
84
|
+
startContentStream,
|
|
85
|
+
startSpinner,
|
|
86
|
+
stopSpinner,
|
|
87
|
+
thinkingPrefix,
|
|
88
|
+
updateSpinner,
|
|
89
|
+
} from './repl-render.mjs';
|
|
90
|
+
import {
|
|
91
|
+
chooseResumeHistoryMode,
|
|
92
|
+
chooseThresholdMode,
|
|
93
|
+
compactCurrentSession,
|
|
94
|
+
confirmCwdSwitch,
|
|
95
|
+
formatResumeCheckpointStatus,
|
|
96
|
+
formatResumeContextStatus,
|
|
97
|
+
listResumableSessions,
|
|
98
|
+
pickResumableSession,
|
|
99
|
+
previewResumeSession,
|
|
100
|
+
renderResumePreview,
|
|
101
|
+
summarizeResumeTranscript,
|
|
102
|
+
} from './repl-resume.mjs';
|
|
103
|
+
import {
|
|
104
|
+
endStatusMarker,
|
|
105
|
+
filterResumeReplayEvents,
|
|
106
|
+
fitAnsiLine,
|
|
107
|
+
formatRelativeTime,
|
|
108
|
+
formatSessionCost,
|
|
109
|
+
historyRoleLabel,
|
|
110
|
+
mergeResumeReplayItems,
|
|
111
|
+
messageCountLabel,
|
|
112
|
+
normalizeResumableSession,
|
|
113
|
+
oneLineInstruction,
|
|
114
|
+
renderHistoryEntries,
|
|
115
|
+
replayStartOrderForMode,
|
|
116
|
+
resumeModeLabel,
|
|
117
|
+
resumeProgressBar,
|
|
118
|
+
resumeTailTurnCount,
|
|
119
|
+
sessionListTimestamp,
|
|
120
|
+
startResumeProgress,
|
|
121
|
+
} from './repl-format.mjs';
|
|
122
|
+
import {
|
|
123
|
+
COMMANDS,
|
|
124
|
+
HELP_GROUPS,
|
|
125
|
+
HELP_GROUP_ALIASES,
|
|
126
|
+
LEGACY_COMMAND_HINTS,
|
|
127
|
+
NAMESPACED_COMMANDS,
|
|
128
|
+
normalizeCommandInput,
|
|
129
|
+
} from '../ui/slash-commands.mjs';
|
|
130
|
+
import { createOrbit } from '../state/orbit.mjs';
|
|
131
|
+
import {
|
|
132
|
+
clearInputPrompt,
|
|
133
|
+
focusDockInput,
|
|
134
|
+
isInputDockMounted,
|
|
135
|
+
mountInputDock,
|
|
136
|
+
moveToContent,
|
|
137
|
+
prepareInputPrompt,
|
|
138
|
+
redrawDockFrame,
|
|
139
|
+
renderDockInput,
|
|
140
|
+
unmountInputDock,
|
|
141
|
+
} from '../ui/input-dock.mjs';
|
|
142
|
+
import { term } from '../ui/term.mjs';
|
|
143
|
+
import { transcriptHeader, transcriptLine } from '../ui/transcript-block.mjs';
|
|
144
|
+
import {
|
|
145
|
+
formatCardHead,
|
|
146
|
+
formatCompactFileDiff,
|
|
147
|
+
summarizeResult,
|
|
148
|
+
recordCard,
|
|
149
|
+
lastCard,
|
|
150
|
+
getCard,
|
|
151
|
+
allCards,
|
|
152
|
+
clearCards,
|
|
153
|
+
} from '../ui/tool-card.mjs';
|
|
154
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
155
|
+
import { paint } from '../ui/palette.mjs';
|
|
156
|
+
import {
|
|
157
|
+
renderSubAgentOpen,
|
|
158
|
+
renderSubAgentClose,
|
|
159
|
+
subAgentIndent,
|
|
160
|
+
inSubAgent as inSubAgentBlock,
|
|
161
|
+
resetSubAgents,
|
|
162
|
+
} from '../ui/sub-agent.mjs';
|
|
163
|
+
|
|
164
|
+
import { createRequire } from 'node:module';
|
|
165
|
+
const __require = createRequire(import.meta.url);
|
|
166
|
+
const VERSION = __require('../../package.json').version;
|
|
167
|
+
|
|
168
|
+
// ── Safe CWD ──
|
|
169
|
+
// If the working directory gets deleted (by a rogue tool call),
|
|
170
|
+
// process.cwd() throws ENOENT. Detect and recover.
|
|
171
|
+
|
|
172
|
+
// safeCwd() moved to ./repl-utils.mjs.
|
|
173
|
+
|
|
174
|
+
// messageCountLabel, sessionListTimestamp, oneLineInstruction, fitAnsiLine
|
|
175
|
+
// moved to ./repl-format.mjs. Imported at the top of this file.
|
|
176
|
+
|
|
177
|
+
// normalizeResumableSession moved to ./repl-format.mjs.
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
// resumeTailTurnCount, replayStartOrderForMode, filterResumeReplayEvents,
|
|
181
|
+
// mergeResumeReplayItems, resumeProgressBar moved to ./repl-format.mjs.
|
|
182
|
+
|
|
183
|
+
// startResumeProgress moved to ./repl-format.mjs.
|
|
184
|
+
|
|
185
|
+
// ── Session State ──
|
|
186
|
+
|
|
187
|
+
// sessionMgrRef.current + orbitRef.current live in ./repl-state.mjs;
|
|
188
|
+
// assigned below at startup by startTerminalRepl().
|
|
189
|
+
|
|
190
|
+
// The `session` object lives in repl-state.mjs so other repl-* modules
|
|
191
|
+
// (resume helpers, tool renderers, streaming, etc.) can import it during
|
|
192
|
+
// the ongoing split. Everything below still references `session.<field>`
|
|
193
|
+
// directly; only the declaration site moved.
|
|
194
|
+
|
|
195
|
+
// ── Commands ──
|
|
196
|
+
|
|
197
|
+
// COMMANDS + HELP_GROUPS + HELP_GROUP_ALIASES + LEGACY_COMMAND_HINTS +
|
|
198
|
+
// NAMESPACED_COMMANDS + normalizeCommandInput are now the canonical
|
|
199
|
+
// catalog in ui/slash-commands.mjs. See imports at the top of this file.
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
function renderHelp(topic = '') {
|
|
203
|
+
const key = String(topic || '').trim().toLowerCase();
|
|
204
|
+
if (!key) {
|
|
205
|
+
process.stderr.write(`\n ${c.bold('Bahulam Code Commands')}\n`);
|
|
206
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
207
|
+
const top = [
|
|
208
|
+
['/help', 'Grouped command help'],
|
|
209
|
+
['/status', 'Session snapshot'],
|
|
210
|
+
['/plan', 'Task list and plan'],
|
|
211
|
+
['/tasks', 'Project task files'],
|
|
212
|
+
['/history', 'Transcript, approvals, undo'],
|
|
213
|
+
['/settings', 'Policy, auth, verbosity'],
|
|
214
|
+
['/why', 'Explain last reasoning'],
|
|
215
|
+
];
|
|
216
|
+
for (const [name, desc] of top) {
|
|
217
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
218
|
+
}
|
|
219
|
+
process.stderr.write(`\n ${c.bold('Categories')}\n`);
|
|
220
|
+
for (const group of HELP_GROUPS) {
|
|
221
|
+
process.stderr.write(` ${c.brand(('/help ' + group.key).padEnd(20))} ${c.dim(group.summary)}\n`);
|
|
222
|
+
}
|
|
223
|
+
process.stderr.write(`\n ${c.dim('Use /help all for legacy command aliases.')}\n`);
|
|
224
|
+
renderKeyboardHelp();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (key === 'all' || key === 'commands') {
|
|
229
|
+
process.stderr.write(`\n ${c.bold('All Commands')}\n`);
|
|
230
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
231
|
+
for (const [name, desc] of Object.entries(COMMANDS)) {
|
|
232
|
+
const alias = LEGACY_COMMAND_HINTS[name] ? c.dim(` alias for ${LEGACY_COMMAND_HINTS[name]}`) : '';
|
|
233
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}${alias}\n`);
|
|
234
|
+
}
|
|
235
|
+
process.stderr.write('\n');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const group = HELP_GROUP_ALIASES.get(key);
|
|
240
|
+
if (!group) {
|
|
241
|
+
process.stderr.write(` ${c.gray(`Unknown help category: ${key}. Use /help.`)}\n`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
process.stderr.write(`\n ${c.bold(group.title)} ${c.dim(group.summary)}\n`);
|
|
246
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
247
|
+
for (const [name, desc] of group.commands) {
|
|
248
|
+
process.stderr.write(` ${c.brand(name.padEnd(30))} ${desc}\n`);
|
|
249
|
+
}
|
|
250
|
+
process.stderr.write('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderKeyboardHelp() {
|
|
254
|
+
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
255
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
256
|
+
process.stderr.write(` ${c.gray('Ctrl+D')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const MODEL_ROLE_ALIASES = new Map([
|
|
260
|
+
['reasoning', 'reasoning'],
|
|
261
|
+
['main', 'reasoning'],
|
|
262
|
+
['coder', 'reasoning'],
|
|
263
|
+
['coding', 'reasoning'],
|
|
264
|
+
['smart', 'reasoning'],
|
|
265
|
+
['fast', 'fast'],
|
|
266
|
+
['explorer', 'fast'],
|
|
267
|
+
['orchestrator', 'orchestrator'],
|
|
268
|
+
['planner', 'orchestrator'],
|
|
269
|
+
['local', 'local'],
|
|
270
|
+
['worker', 'worker'],
|
|
271
|
+
['explore', 'explore'],
|
|
272
|
+
['plan', 'plan'],
|
|
273
|
+
['verify', 'verify'],
|
|
274
|
+
['debug', 'debug'],
|
|
275
|
+
['refactor', 'refactor'],
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const MODEL_ROLE_LABELS = {
|
|
279
|
+
reasoning: 'coding',
|
|
280
|
+
fast: 'fast',
|
|
281
|
+
orchestrator: 'orchestrator',
|
|
282
|
+
local: 'local',
|
|
283
|
+
worker: 'worker',
|
|
284
|
+
explore: 'explore',
|
|
285
|
+
plan: 'plan',
|
|
286
|
+
verify: 'verify',
|
|
287
|
+
debug: 'debug',
|
|
288
|
+
refactor: 'refactor',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const MODEL_ROLE_ORDER = [
|
|
292
|
+
'reasoning',
|
|
293
|
+
'fast',
|
|
294
|
+
'orchestrator',
|
|
295
|
+
'local',
|
|
296
|
+
'worker',
|
|
297
|
+
'explore',
|
|
298
|
+
'plan',
|
|
299
|
+
'verify',
|
|
300
|
+
'debug',
|
|
301
|
+
'refactor',
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
function normalizeModelRole(value) {
|
|
305
|
+
return MODEL_ROLE_ALIASES.get(String(value || '').trim().toLowerCase()) || null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function sessionModelOverrideEntries() {
|
|
309
|
+
return Object.entries(session.modelOverrides || {})
|
|
310
|
+
.filter(([, model]) => typeof model === 'string' && model.trim())
|
|
311
|
+
.sort(([a], [b]) => MODEL_ROLE_ORDER.indexOf(a) - MODEL_ROLE_ORDER.indexOf(b));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function printModelCommandUsage() {
|
|
315
|
+
process.stderr.write(` ${c.gray('Usage:')} /model [model]\n`);
|
|
316
|
+
process.stderr.write(` /model <role> <model>\n`);
|
|
317
|
+
process.stderr.write(` /model clear [role]\n`);
|
|
318
|
+
process.stderr.write(` ${c.gray('Roles:')} ${MODEL_ROLE_ORDER.map(role => MODEL_ROLE_LABELS[role]).join(', ')}\n`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function printModelStatus() {
|
|
322
|
+
process.stderr.write(`\n ${c.bold('Models')}\n`);
|
|
323
|
+
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
324
|
+
process.stderr.write(` ${c.gray('Active coding')} ${session.model || 'backend default'}\n`);
|
|
325
|
+
|
|
326
|
+
const limits = session.modelLimits || {};
|
|
327
|
+
const rows = [
|
|
328
|
+
['coder', limits.coder?.model],
|
|
329
|
+
['explorer', limits.explorer?.model],
|
|
330
|
+
['orchestrator', limits.orchestrator?.model],
|
|
331
|
+
].filter(([, model]) => model);
|
|
332
|
+
if (rows.length) {
|
|
333
|
+
process.stderr.write(`\n ${c.bold('Backend roles')}\n`);
|
|
334
|
+
for (const [role, model] of rows) {
|
|
335
|
+
process.stderr.write(` ${c.brand(role.padEnd(14))} ${c.dim(model)}\n`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const overrides = sessionModelOverrideEntries();
|
|
340
|
+
process.stderr.write(`\n ${c.bold('Session overrides')}\n`);
|
|
341
|
+
if (!overrides.length) {
|
|
342
|
+
process.stderr.write(` ${c.dim('(none)')}\n`);
|
|
343
|
+
} else {
|
|
344
|
+
for (const [role, model] of overrides) {
|
|
345
|
+
process.stderr.write(` ${c.brand((MODEL_ROLE_LABELS[role] || role).padEnd(14))} ${model}\n`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
process.stderr.write('\n');
|
|
349
|
+
printModelCommandUsage();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleModelCommand(rest = '') {
|
|
353
|
+
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
354
|
+
if (parts.length === 0) {
|
|
355
|
+
printModelStatus();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (parts[0] === 'clear' || parts[0] === 'reset') {
|
|
360
|
+
if (parts.length === 1) {
|
|
361
|
+
session.modelOverrides = {};
|
|
362
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Cleared all session model overrides.')}\n`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const role = normalizeModelRole(parts[1]);
|
|
366
|
+
if (!role) {
|
|
367
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Unknown model role: ${parts[1]}`)}\n`);
|
|
368
|
+
printModelCommandUsage();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
delete session.modelOverrides[role];
|
|
372
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Cleared ${MODEL_ROLE_LABELS[role] || role} model override.`)}\n`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let role = 'reasoning';
|
|
377
|
+
let model = parts.join(' ');
|
|
378
|
+
const maybeRole = normalizeModelRole(parts[0]);
|
|
379
|
+
if (maybeRole && parts.length >= 2) {
|
|
380
|
+
role = maybeRole;
|
|
381
|
+
model = parts.slice(1).join(' ');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!model || normalizeModelRole(model)) {
|
|
385
|
+
printModelCommandUsage();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
session.modelOverrides = { ...(session.modelOverrides || {}), [role]: model };
|
|
390
|
+
if (role === 'reasoning') session.model = model;
|
|
391
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Session ${MODEL_ROLE_LABELS[role] || role} model override:`)} ${c.brand(model)}\n`);
|
|
392
|
+
process.stderr.write(` ${c.dim('Use /model clear or /model clear <role> to return to backend settings.')}\n`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function stripPathQuotes(value) {
|
|
396
|
+
const text = String(value || '').trim();
|
|
397
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
398
|
+
return text.slice(1, -1);
|
|
399
|
+
}
|
|
400
|
+
return text;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function pendingVisionPaths(ctx) {
|
|
404
|
+
if (!Array.isArray(ctx.pendingVisionPaths)) ctx.pendingVisionPaths = [];
|
|
405
|
+
return ctx.pendingVisionPaths;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function printPendingAttachments(ctx) {
|
|
409
|
+
const pending = pendingVisionPaths(ctx);
|
|
410
|
+
if (!pending.length) {
|
|
411
|
+
process.stderr.write(` ${c.gray('No pending image attachments.')}\n`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
process.stderr.write(`\n ${c.bold('Pending Attachments')}\n`);
|
|
415
|
+
for (const filePath of pending) {
|
|
416
|
+
process.stderr.write(` ${c.brand('◇')} ${filePath}\n`);
|
|
417
|
+
}
|
|
418
|
+
process.stderr.write(` ${c.dim('They will be sent for vision analysis with your next prompt.')}\n`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleAttachCommand(rest = '', ctx) {
|
|
422
|
+
const pending = pendingVisionPaths(ctx);
|
|
423
|
+
const value = stripPathQuotes(rest);
|
|
424
|
+
if (!value) {
|
|
425
|
+
process.stderr.write(` ${c.yellow('Usage:')} /attach <image-path> ${c.dim('or')} /attach clipboard\n`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (value === 'clear') {
|
|
429
|
+
pending.length = 0;
|
|
430
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Cleared pending image attachments.')}\n`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (['clipboard', '--clipboard', 'paste', '--paste'].includes(value.toLowerCase())) {
|
|
434
|
+
try {
|
|
435
|
+
const filePath = writeClipboardImageToTemp();
|
|
436
|
+
pending.push(filePath);
|
|
437
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('attached clipboard image for next prompt:')} ${c.brand(path.basename(filePath))}\n`);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim(err.message || String(err))}\n`);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const resolved = resolveAttachmentPath(value, safeCwd());
|
|
444
|
+
pending.push(resolved);
|
|
445
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('attached for next prompt:')} ${c.brand(path.basename(resolved))}\n`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function handleAttachmentsCommand(rest = '', ctx) {
|
|
449
|
+
const action = String(rest || '').trim().toLowerCase();
|
|
450
|
+
if (action === 'clear') {
|
|
451
|
+
pendingVisionPaths(ctx).length = 0;
|
|
452
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Cleared pending image attachments.')}\n`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
printPendingAttachments(ctx);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function confirmVisionUpload(ctx, attachments, { skip = false } = {}) {
|
|
459
|
+
if (skip || process.env.KEPLER_VISION_CONFIRM === '0' || process.env.KEPLER_VISION_CONFIRM === 'false') {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (!ctx?._rl || !process.stdin.isTTY) return false;
|
|
463
|
+
const names = attachments.map(a => a.name).join(', ');
|
|
464
|
+
return await new Promise(resolve => {
|
|
465
|
+
ctx._rl.question(` ${c.yellow('Upload image for vision analysis?')} ${c.dim(names)} ${c.dim('[y/N]')} `, answer => {
|
|
466
|
+
resolve(/^y(?:es)?$/i.test(String(answer || '').trim()));
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseSimpleFlags(parts) {
|
|
472
|
+
const flags = {};
|
|
473
|
+
const positional = [];
|
|
474
|
+
for (let i = 0; i < parts.length; i++) {
|
|
475
|
+
const part = parts[i];
|
|
476
|
+
if (part.startsWith('--')) {
|
|
477
|
+
const key = part.slice(2);
|
|
478
|
+
const next = parts[i + 1];
|
|
479
|
+
if (!next || next.startsWith('--')) {
|
|
480
|
+
flags[key] = true;
|
|
481
|
+
} else {
|
|
482
|
+
flags[key] = next;
|
|
483
|
+
i++;
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
positional.push(part);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { flags, positional };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function printAgentsUsage() {
|
|
493
|
+
process.stderr.write(` ${c.gray('Usage:')} /agents\n`);
|
|
494
|
+
process.stderr.write(` /agents create <name> [--description text] [--role specialist] [--model id] [--tools a,b] [--open|--no-open]\n`);
|
|
495
|
+
process.stderr.write(` /agents edit <name>\n`);
|
|
496
|
+
process.stderr.write(` /agents sync [name]\n`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function printAgentsList() {
|
|
500
|
+
const local = listLocalAgents(safeCwd());
|
|
501
|
+
process.stderr.write(`\n ${c.bold('Built-in Agents')}\n`);
|
|
502
|
+
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
503
|
+
for (const agent of BUILTIN_AGENTS) {
|
|
504
|
+
process.stderr.write(` ${c.brand(('/' + agent.command).padEnd(14))} ${agent.description}\n`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
process.stderr.write(`\n ${c.bold('Local Agents')} ${c.dim('.bahulam/agents + ~/.bahulam/agents')}\n`);
|
|
508
|
+
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
509
|
+
if (!local.length) {
|
|
510
|
+
process.stderr.write(` ${c.dim('(none)')}\n`);
|
|
511
|
+
} else {
|
|
512
|
+
for (const agent of local) {
|
|
513
|
+
const scope = agent.source_scope === 'project' ? c.green('project') : c.dim(agent.source_scope);
|
|
514
|
+
const model = agent.model ? c.dim(` · ${agent.model}`) : '';
|
|
515
|
+
process.stderr.write(` ${c.brand(agent.slug.padEnd(18))} ${scope} ${agent.description || ''}${model}\n`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
process.stderr.write('\n');
|
|
519
|
+
printAgentsUsage();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function handleAgentsCommand(rest = '', ctx) {
|
|
523
|
+
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
524
|
+
const action = (parts.shift() || 'list').toLowerCase();
|
|
525
|
+
|
|
526
|
+
if (action === 'list' || action === 'ls') {
|
|
527
|
+
printAgentsList();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (action === 'create' || action === 'new') {
|
|
532
|
+
const { flags, positional } = parseSimpleFlags(parts);
|
|
533
|
+
const name = positional[0];
|
|
534
|
+
if (!name) {
|
|
535
|
+
printAgentsUsage();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const created = createAgentFile({
|
|
540
|
+
cwd: safeCwd(),
|
|
541
|
+
name,
|
|
542
|
+
description: flags.description || flags.desc || '',
|
|
543
|
+
role: flags.role || 'specialist',
|
|
544
|
+
model: flags.model || '',
|
|
545
|
+
tools: flags.tools || 'read_file,search_code,list_files',
|
|
546
|
+
force: Boolean(flags.force),
|
|
547
|
+
});
|
|
548
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Created local agent:')} ${created.filePath}\n`);
|
|
549
|
+
const shouldOpen = !flags['no-open'] && (Boolean(flags.open) || isVsCodeTerminal());
|
|
550
|
+
if (shouldOpen) {
|
|
551
|
+
const opened = openAgentFile(created.filePath, {
|
|
552
|
+
allowConfiguredEditor: Boolean(flags.open),
|
|
553
|
+
});
|
|
554
|
+
if (opened.opened) {
|
|
555
|
+
process.stderr.write(` ${c.dim('Opened in:')} ${opened.editor}\n`);
|
|
556
|
+
} else {
|
|
557
|
+
process.stderr.write(` ${c.dim(opened.reason)}\n`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
process.stderr.write(` ${c.dim('Sync explicitly with:')} /agents sync ${created.slug}\n`);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (action === 'edit' || action === 'open') {
|
|
568
|
+
const target = parts.find(p => !p.startsWith('--'));
|
|
569
|
+
if (!target) {
|
|
570
|
+
printAgentsUsage();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const local = listLocalAgents(safeCwd());
|
|
574
|
+
const agent = local.find(item => item.slug === target || item.name === target);
|
|
575
|
+
if (!agent?.source) {
|
|
576
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`No local agent found: ${target}`)}\n`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const opened = openAgentFile(agent.source, { allowConfiguredEditor: true });
|
|
580
|
+
if (opened.opened) {
|
|
581
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Opened ${agent.slug} in ${opened.editor}.`)}\n`);
|
|
582
|
+
} else {
|
|
583
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(opened.reason)}\n`);
|
|
584
|
+
process.stderr.write(` ${c.dim('Agent file:')} ${agent.source}\n`);
|
|
585
|
+
}
|
|
586
|
+
process.stderr.write(` ${c.dim('Sync after editing:')} /agents sync ${agent.slug}\n`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (action === 'sync') {
|
|
591
|
+
const target = parts.find(p => !p.startsWith('--'));
|
|
592
|
+
const local = listLocalAgents(safeCwd());
|
|
593
|
+
const selected = target
|
|
594
|
+
? local.filter(agent => agent.slug === target || agent.name === target)
|
|
595
|
+
: local;
|
|
596
|
+
if (!selected.length) {
|
|
597
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(target ? `No local agent found: ${target}` : 'No local agents to sync.')}\n`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const creds = ctx.auth.loadCredentials();
|
|
602
|
+
const result = await syncAgentsToBackend({
|
|
603
|
+
backendUrl: creds.backendUrl,
|
|
604
|
+
token: creds.token,
|
|
605
|
+
agents: selected,
|
|
606
|
+
});
|
|
607
|
+
const synced = result.synced ?? selected.length;
|
|
608
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to Supabase.`)}\n`);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
printAgentsUsage();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function commandCompletions(line) {
|
|
619
|
+
if (line.startsWith('/help ')) {
|
|
620
|
+
const topic = line.slice('/help '.length).toLowerCase();
|
|
621
|
+
const categories = ['all', ...HELP_GROUPS.map(g => g.key)];
|
|
622
|
+
const hits = categories.map(c => `/help ${c}`).filter(cmd => cmd.startsWith(`/help ${topic}`));
|
|
623
|
+
return hits.length ? hits : categories.map(c => `/help ${c}`);
|
|
624
|
+
}
|
|
625
|
+
const top = ['/help', '/status', '/plan', '/tasks', '/history', '/settings', '/why'];
|
|
626
|
+
const namespaced = HELP_GROUPS.flatMap(g => g.commands.map(([name]) => name.split(/\s+/)[0]));
|
|
627
|
+
const all = [...new Set([...top, ...namespaced, ...Object.keys(COMMANDS), '/quit'])].sort();
|
|
628
|
+
const hits = all.filter(cmd => cmd.startsWith(line));
|
|
629
|
+
return hits.length ? hits : all;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function slashCommandSuggestions(line, limit = 5) {
|
|
633
|
+
const text = String(line || '').trimStart();
|
|
634
|
+
if (!text.startsWith('/')) return [];
|
|
635
|
+
const partial = text.split(/\s+/)[0] || '/';
|
|
636
|
+
return commandCompletions(partial)
|
|
637
|
+
.filter(cmd => cmd.startsWith('/'))
|
|
638
|
+
.slice(0, limit)
|
|
639
|
+
.map(cmd => ({
|
|
640
|
+
command: cmd,
|
|
641
|
+
description: COMMANDS[cmd] || (cmd === '/quit' ? 'Exit CLI' : ''),
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Banner ──
|
|
646
|
+
|
|
647
|
+
function printBanner(auth) {
|
|
648
|
+
// The visual block itself lives in the branded banner module. The trailing
|
|
649
|
+
// status line stays here because it needs `auth`.
|
|
650
|
+
printBrandedBanner(VERSION);
|
|
651
|
+
|
|
652
|
+
const creds = auth.loadCredentials();
|
|
653
|
+
const env = process.env.TARANG_ENV || 'production';
|
|
654
|
+
const authStatus = creds.token ? c.green('authenticated') : c.red('/login to start');
|
|
655
|
+
process.stderr.write(` ${c.dim(env)} ${authStatus}\n\n`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── Prompt Chrome ──
|
|
659
|
+
//
|
|
660
|
+
// Design: let the content breathe. The prompt area is a thin contextual
|
|
661
|
+
// strip — only shows what changed since last turn. No heavy borders.
|
|
662
|
+
//
|
|
663
|
+
// Layout after a response:
|
|
664
|
+
//
|
|
665
|
+
// <assistant content>
|
|
666
|
+
//
|
|
667
|
+
// ✓ 3 tools · 1.2s · $0.02 ctx 21% · 42k tok
|
|
668
|
+
// ╶─────────────────────────────────────────────────────────────────╴
|
|
669
|
+
// kepler ›
|
|
670
|
+
//
|
|
671
|
+
// Layout on first prompt (no stats yet):
|
|
672
|
+
//
|
|
673
|
+
// ╶─────────────────────────────────────────────────────────────────╴
|
|
674
|
+
// kepler ›
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Build the contextual status strip — compact, one line.
|
|
678
|
+
* Left side: last-turn summary (tools, time, cost)
|
|
679
|
+
* Right side: session totals (ctx%, tokens)
|
|
680
|
+
*/
|
|
681
|
+
function computeCacheTotals() {
|
|
682
|
+
let read = 0;
|
|
683
|
+
let write = 0;
|
|
684
|
+
for (const b of session.costBreakdown) {
|
|
685
|
+
read += b.cache_read_tokens || 0;
|
|
686
|
+
write += b.cache_creation_tokens || 0;
|
|
687
|
+
}
|
|
688
|
+
// OpenRouter/Anthropic/DeepSeek return `total_input_tokens` INCLUSIVE of
|
|
689
|
+
// cache-read tokens. session.inputTokens is that sum, so the denominator is
|
|
690
|
+
// just session.inputTokens (do NOT add `read` — would double-count).
|
|
691
|
+
const hitRate = session.inputTokens > 0
|
|
692
|
+
? Math.round((read / session.inputTokens) * 100)
|
|
693
|
+
: 0;
|
|
694
|
+
return { read, write, hitRate };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function buildContextStrip() {
|
|
698
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
699
|
+
const elapsed = formatElapsed(session.startTime);
|
|
700
|
+
// Cache hit % lives under /cache — keep the always-on strip focused on
|
|
701
|
+
// volume + elapsed. Historical rate calc was double-counting the cache tokens
|
|
702
|
+
// vs OpenRouter's convention (see computeCacheTotals) which was misleading.
|
|
703
|
+
const right = [
|
|
704
|
+
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
705
|
+
c.dim(elapsed),
|
|
706
|
+
].join(c.dim(' · '));
|
|
707
|
+
|
|
708
|
+
return right;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Dock meta line (model · cwd ⎇ branch · turn N) ─────────────────────
|
|
712
|
+
//
|
|
713
|
+
// The dock's meta row shows durable session context. Git branch is cached
|
|
714
|
+
// so we don't shell out on every keystroke; refreshed at most every 5s.
|
|
715
|
+
|
|
716
|
+
const _dockGitCache = { branch: null, at: 0, cwd: null };
|
|
717
|
+
|
|
718
|
+
function _probeGitBranch(cwd) {
|
|
719
|
+
const now = Date.now();
|
|
720
|
+
if (_dockGitCache.cwd === cwd && (now - _dockGitCache.at) < 5000) {
|
|
721
|
+
return _dockGitCache.branch;
|
|
722
|
+
}
|
|
723
|
+
let branch = null;
|
|
724
|
+
try {
|
|
725
|
+
// Synchronous but bounded: git head lookup is a single file read.
|
|
726
|
+
branch = _execSync('git branch --show-current', {
|
|
727
|
+
cwd, encoding: 'utf-8', timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
|
|
728
|
+
}).trim() || null;
|
|
729
|
+
} catch { branch = null; }
|
|
730
|
+
_dockGitCache.cwd = cwd;
|
|
731
|
+
_dockGitCache.at = now;
|
|
732
|
+
_dockGitCache.branch = branch;
|
|
733
|
+
return branch;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function buildDockMeta() {
|
|
737
|
+
const parts = [];
|
|
738
|
+
|
|
739
|
+
const cwd = process.cwd();
|
|
740
|
+
const projectName = path.basename(cwd);
|
|
741
|
+
const branch = _probeGitBranch(cwd);
|
|
742
|
+
parts.push(branch ? `${projectName} ⎇ ${branch}` : projectName);
|
|
743
|
+
|
|
744
|
+
if (session.turns > 0) {
|
|
745
|
+
parts.push(`turn ${session.turns}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
749
|
+
if (totalTokens > 0) {
|
|
750
|
+
parts.push(`${formatTokens(totalTokens)} tok`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return parts.join(' · ');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Print the prompt separator + prompt label.
|
|
758
|
+
* Minimal horizontal rule with contextual info.
|
|
759
|
+
*/
|
|
760
|
+
function printPromptBlock() {
|
|
761
|
+
const w = process.stdout.columns || 80;
|
|
762
|
+
const strip = buildContextStrip();
|
|
763
|
+
const stripPlain = stripAnsi(strip);
|
|
764
|
+
|
|
765
|
+
// Rule with context strip right-aligned
|
|
766
|
+
const ruleLen = Math.max(0, w - stripPlain.length - 4);
|
|
767
|
+
process.stderr.write(
|
|
768
|
+
c.dim('╶') + c.dim('─'.repeat(ruleLen)) + ' ' + strip + ' ' + c.dim('╴') + '\n'
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Print a turn summary after a response completes.
|
|
774
|
+
* Shows only when there's something meaningful to report.
|
|
775
|
+
*/
|
|
776
|
+
/**
|
|
777
|
+
* Pull blocker bullet points from the completion payload — used by the
|
|
778
|
+
* failure variant of the mission report.
|
|
779
|
+
*/
|
|
780
|
+
function extractBlockers(data) {
|
|
781
|
+
const out = [];
|
|
782
|
+
if (data?.error) out.push(String(data.error).slice(0, 160));
|
|
783
|
+
if (Array.isArray(data?.failed_tests)) {
|
|
784
|
+
for (const t of data.failed_tests.slice(0, 6)) {
|
|
785
|
+
if (typeof t === 'string') out.push(t);
|
|
786
|
+
else if (t?.name) out.push(`${t.name}${t.message ? ': ' + t.message : ''}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return out;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function printTurnSummary(toolCount, durationS, turnCost) {
|
|
793
|
+
const parts = [];
|
|
794
|
+
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
795
|
+
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
796
|
+
if (parts.length > 0) {
|
|
797
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
798
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
799
|
+
runtime.lastRenderedBlock = 'status';
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function formatMessageChip(rateLimit) {
|
|
804
|
+
const remaining = messagesRemaining(rateLimit);
|
|
805
|
+
if (remaining === Infinity) return 'unlimited messages';
|
|
806
|
+
const limit = Number(rateLimit?.msgs_per_window);
|
|
807
|
+
if (typeof remaining === 'number' && Number.isFinite(limit)) {
|
|
808
|
+
return `${remaining}/${limit} messages`;
|
|
809
|
+
}
|
|
810
|
+
return 'messages';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function updateStatusBar() {
|
|
814
|
+
// No-op: status is printed inline via printPromptBlock before each prompt
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Tool Display Renderer ──
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Render a tool call as the head of a Mission Control card — icon + label +
|
|
821
|
+
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
822
|
+
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
823
|
+
*/
|
|
824
|
+
// Deferred-head strategy: we DON'T print the tool head when tool_call fires.
|
|
825
|
+
// Instead we buffer it and let renderToolResult emit one combined line
|
|
826
|
+
// "head → outcome · duration\n". A spinner shows what's running in the
|
|
827
|
+
// meantime so the user still has feedback during slow tools.
|
|
828
|
+
//
|
|
829
|
+
// If something else needs to print before the result arrives (a streamed
|
|
830
|
+
// content event, a sub-agent open, an error, completion), we flush the
|
|
831
|
+
// buffered head as a regular two-line shape first so the interleaving
|
|
832
|
+
// content lands below it.
|
|
833
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
834
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
835
|
+
// Explore-run collapse: reduces bursts of list/read/search/index tool calls
|
|
836
|
+
// into one in-place summary line so the user sees the agent's PROGRESS
|
|
837
|
+
// (12 files listed, 8 read, latest name) instead of a wall of individual
|
|
838
|
+
// tool cards. Any non-explore event flushes it to a static line so the
|
|
839
|
+
// summary survives when the transcript scrolls.
|
|
840
|
+
// Mutable run state stays here for now — see repl-explore.mjs for the pure
|
|
841
|
+
// classifier. Split TBD.
|
|
842
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
// ── Event Renderer ──
|
|
846
|
+
|
|
847
|
+
function renderEvent(event) {
|
|
848
|
+
const { type, data } = event;
|
|
849
|
+
|
|
850
|
+
// Push every event into the orbit state machine before rendering so phase
|
|
851
|
+
// and cost state stay current for prompt/context surfaces.
|
|
852
|
+
if (orbitRef.current) orbitRef.current.onEvent(event);
|
|
853
|
+
|
|
854
|
+
// If we've been collapsing explore tools into a summary spinner, an
|
|
855
|
+
// incoming event that will actually WRITE to the screen ends the run
|
|
856
|
+
// and freezes the spinner into a static one-line summary. Transient
|
|
857
|
+
// metadata events (thinking, spinner-only status, sub_agent_tool,
|
|
858
|
+
// worker/phase updates, session_info) don't write and must NOT flush —
|
|
859
|
+
// otherwise every sub-agent tool call fires a `sub_agent_tool` event
|
|
860
|
+
// right before its `tool_call`, splitting each burst into a fresh line.
|
|
861
|
+
if (runtime.exploreRun.lineActive) {
|
|
862
|
+
const isExploreEvent =
|
|
863
|
+
(type === 'tool_call' || type === 'tool_request' ||
|
|
864
|
+
type === 'tool_result' || type === 'tool_done') &&
|
|
865
|
+
isExploreTool(data?.tool);
|
|
866
|
+
const isTransientEvent =
|
|
867
|
+
type === 'thinking' || // may or may not render text
|
|
868
|
+
type === 'status' || // usually just updates the spinner
|
|
869
|
+
type === 'sub_agent_tool' || // metadata for the tool_call to follow
|
|
870
|
+
type === 'worker_update' ||
|
|
871
|
+
type === 'phase_update' ||
|
|
872
|
+
type === 'phase_summary' ||
|
|
873
|
+
type === 'phase_start' ||
|
|
874
|
+
type === 'worker_start' ||
|
|
875
|
+
type === 'worker_done' ||
|
|
876
|
+
type === 'session_info';
|
|
877
|
+
if (!isExploreEvent && !isTransientEvent) flushExploreRun();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
switch (type) {
|
|
881
|
+
case 'status': {
|
|
882
|
+
const msg = data?.message || '';
|
|
883
|
+
if (!msg || msg === 'Agent started') return;
|
|
884
|
+
if (/^Stagnation:/i.test(msg)) {
|
|
885
|
+
renderStagnation(data);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
startSpinner(msg);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
case 'stagnation':
|
|
893
|
+
case 'stagnation_detected': {
|
|
894
|
+
renderStagnation(data);
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
case 'thinking': {
|
|
899
|
+
const text = data?.message || data?.text || '';
|
|
900
|
+
if (text && !text.startsWith('Processing')) {
|
|
901
|
+
// Surface substantive thinking text as visible prose so the user can
|
|
902
|
+
// follow the agent's reasoning, not just see a spinner blip. We
|
|
903
|
+
// print at most one line per distinct thought, dim italic.
|
|
904
|
+
if (text.length > 12 && text !== session._lastEmittedThinking) {
|
|
905
|
+
flushContent();
|
|
906
|
+
flushPendingHead();
|
|
907
|
+
stopSpinner();
|
|
908
|
+
renderBlockBoundary('thinking');
|
|
909
|
+
process.stderr.write(` ${c.dim(thinkingPrefix(text) + ' · ')}${c.italic(c.dim(clippedThinking(text)))}\n`);
|
|
910
|
+
runtime.lastRenderedBlock = 'thinking';
|
|
911
|
+
session._lastEmittedThinking = text;
|
|
912
|
+
}
|
|
913
|
+
startSpinner(text.slice(0, 80));
|
|
914
|
+
// Capture reasoning so /why can replay it.
|
|
915
|
+
session.lastReasoning = text;
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
case 'content': {
|
|
921
|
+
let text = data?.text || '';
|
|
922
|
+
if (text) {
|
|
923
|
+
flushContent();
|
|
924
|
+
stopSpinner();
|
|
925
|
+
if (runtime.streamedPartialText && text.startsWith(runtime.streamedPartialText)) {
|
|
926
|
+
text = text.slice(runtime.streamedPartialText.length);
|
|
927
|
+
} else if (runtime.streamedPartialText.includes(text)) {
|
|
928
|
+
text = '';
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (text) {
|
|
932
|
+
renderBlockBoundary('content');
|
|
933
|
+
if (!runtime.contentHeaderPrinted) {
|
|
934
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
935
|
+
runtime.contentHeaderPrinted = true;
|
|
936
|
+
}
|
|
937
|
+
const rendered = renderMarkdown(text);
|
|
938
|
+
for (const line of rendered.split('\n')) {
|
|
939
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
940
|
+
}
|
|
941
|
+
runtime.renderedContentThisTurn = true;
|
|
942
|
+
runtime.lastRenderedBlock = 'content';
|
|
943
|
+
}
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
case 'reconnecting': {
|
|
948
|
+
stopSpinner();
|
|
949
|
+
flushContent();
|
|
950
|
+
flushPendingHead();
|
|
951
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
952
|
+
const attempt = data?.attempt ? `attempt ${data.attempt}` : 'reconnecting';
|
|
953
|
+
const delayMs = Number(data?.delay_ms || 0);
|
|
954
|
+
const wait = delayMs > 0
|
|
955
|
+
? ` · retrying in ${delayMs < 1000 ? `${delayMs}ms` : `${(delayMs / 1000).toFixed(delayMs < 10_000 ? 1 : 0)}s`}`
|
|
956
|
+
: '';
|
|
957
|
+
const after = data?.after != null ? ` from event ${data.after}` : '';
|
|
958
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`connection lost; ${attempt}${wait}${after}`)}\n`);
|
|
959
|
+
runtime.lastRenderedBlock = 'status';
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
case 'reconnected': {
|
|
964
|
+
stopSpinner();
|
|
965
|
+
flushContent();
|
|
966
|
+
flushPendingHead();
|
|
967
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
968
|
+
const replayed = data?.replayed != null ? ` · replayed ${data.replayed} events` : '';
|
|
969
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`reconnected${replayed}`)}\n`);
|
|
970
|
+
runtime.lastRenderedBlock = 'status';
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
case 'reconnect_failed': {
|
|
975
|
+
stopSpinner();
|
|
976
|
+
flushContent();
|
|
977
|
+
flushPendingHead();
|
|
978
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
979
|
+
const message = data?.message || 'connection lost and reconnect failed. Use /resume to continue from saved history.';
|
|
980
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim(message)}\n`);
|
|
981
|
+
runtime.lastRenderedBlock = 'status';
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
case 'content_partial': {
|
|
986
|
+
const text = data?.text || '';
|
|
987
|
+
if (text) {
|
|
988
|
+
stopSpinner();
|
|
989
|
+
appendContent(text);
|
|
990
|
+
}
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
case 'tool_call':
|
|
995
|
+
case 'tool_request': {
|
|
996
|
+
const isInternal = Boolean(data?.internal || data?.sub_agent);
|
|
997
|
+
if (isInternal) {
|
|
998
|
+
session.subAgentToolCalls++;
|
|
999
|
+
session.totalSubAgentToolCalls++;
|
|
1000
|
+
} else {
|
|
1001
|
+
session.toolCalls++;
|
|
1002
|
+
session.totalPrimaryToolCalls++;
|
|
1003
|
+
}
|
|
1004
|
+
session.totalToolCalls++;
|
|
1005
|
+
stopSpinner();
|
|
1006
|
+
flushContent();
|
|
1007
|
+
renderToolCall(data);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ── HITL: Framework-level approval events ──
|
|
1012
|
+
|
|
1013
|
+
case 'approval_required': {
|
|
1014
|
+
stopSpinner();
|
|
1015
|
+
flushContent();
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
case 'approval_granted': {
|
|
1020
|
+
// Human approvals are rendered by approval.mjs. Auto-read grants are
|
|
1021
|
+
// otherwise invisible, so show one dim confirmation before the tool card.
|
|
1022
|
+
const scope = data?.grant_scope || data?.scope || '';
|
|
1023
|
+
if (scope === 'auto_read') {
|
|
1024
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1025
|
+
const toolName = data?.tool || data?.tool_name || '';
|
|
1026
|
+
const args = data?.args || data?.input || {};
|
|
1027
|
+
const summary = toolDisplaySummary(toolName, args);
|
|
1028
|
+
const label = toolDisplayLabel(toolName);
|
|
1029
|
+
const subject = summary ? `${label} ${summary}` : label;
|
|
1030
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`${subject} · auto-approved read`)}\n`);
|
|
1031
|
+
runtime.lastRenderedBlock = 'status';
|
|
1032
|
+
}
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
case 'approval_denied': {
|
|
1037
|
+
const reason = data?.reason || 'User denied';
|
|
1038
|
+
const toolName = data?.tool || '';
|
|
1039
|
+
const indent = subAgentIndent();
|
|
1040
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1041
|
+
process.stderr.write(`${indent}${c.red('✗')} ${c.dim(`Denied ${toolName}: ${reason}`)}\n`);
|
|
1042
|
+
runtime.lastRenderedBlock = 'status';
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
case 'tool_result':
|
|
1047
|
+
case 'tool_done': {
|
|
1048
|
+
stopSpinner();
|
|
1049
|
+
renderToolResult(data, type);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
case 'plan': {
|
|
1054
|
+
stopSpinner();
|
|
1055
|
+
flushContent();
|
|
1056
|
+
const milestones = data?.milestones || data?.steps || [];
|
|
1057
|
+
const title = data?.title || 'Plan';
|
|
1058
|
+
renderBlockBoundary('plan');
|
|
1059
|
+
process.stderr.write(` ${c.brand('▸')} ${c.bold(title)}\n`);
|
|
1060
|
+
for (const [index, milestone] of milestones.entries()) {
|
|
1061
|
+
const label = typeof milestone === 'string'
|
|
1062
|
+
? milestone
|
|
1063
|
+
: milestone.name || milestone.title || milestone.description || `Step ${index + 1}`;
|
|
1064
|
+
const status = typeof milestone === 'object' ? milestone.status : '';
|
|
1065
|
+
const marker = status === 'complete' || status === 'completed' ? c.green('✓') : c.dim(`${index + 1}.`);
|
|
1066
|
+
process.stderr.write(` ${marker} ${label}\n`);
|
|
1067
|
+
}
|
|
1068
|
+
runtime.lastRenderedBlock = 'plan';
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
case 'change': {
|
|
1073
|
+
stopSpinner();
|
|
1074
|
+
const changeType = data?.type || 'modify';
|
|
1075
|
+
const filePath = shortPath(data?.path || '');
|
|
1076
|
+
const icon = changeType === 'create' ? c.green('+') :
|
|
1077
|
+
changeType === 'delete' ? c.red('-') : c.yellow('~');
|
|
1078
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1079
|
+
process.stderr.write(` ${icon} ${c.dim(filePath)}\n`);
|
|
1080
|
+
runtime.lastRenderedBlock = 'status';
|
|
1081
|
+
// Track changed files
|
|
1082
|
+
if (filePath && !session.filesChanged.includes(filePath)) {
|
|
1083
|
+
session.filesChanged.push(filePath);
|
|
1084
|
+
}
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
case 'phase_start':
|
|
1089
|
+
case 'phase_update': {
|
|
1090
|
+
const phase = data?.phase || data?.stage_name || '';
|
|
1091
|
+
if (phase) {
|
|
1092
|
+
stopSpinner();
|
|
1093
|
+
session.phases.push({ name: phase, time: Date.now() });
|
|
1094
|
+
renderBlockBoundary('plan');
|
|
1095
|
+
process.stderr.write(` ${c.brand('▸')} ${c.bold(phase)}\n`);
|
|
1096
|
+
runtime.lastRenderedBlock = 'plan';
|
|
1097
|
+
}
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
case 'phase_summary': {
|
|
1102
|
+
const summary = data?.summary || '';
|
|
1103
|
+
if (summary) {
|
|
1104
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1105
|
+
process.stderr.write(` ${c.dim(summary.slice(0, 120))}\n`);
|
|
1106
|
+
runtime.lastRenderedBlock = 'status';
|
|
1107
|
+
}
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
case 'worker_start':
|
|
1112
|
+
case 'worker_update': {
|
|
1113
|
+
const worker = data?.worker || data?.name || '';
|
|
1114
|
+
const status = data?.status || data?.message || 'working';
|
|
1115
|
+
if (worker) startSpinner(`${worker}: ${status}`);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
case 'worker_done': {
|
|
1120
|
+
stopSpinner();
|
|
1121
|
+
const worker = data?.worker || data?.name || '';
|
|
1122
|
+
if (worker) {
|
|
1123
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1124
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(worker)}\n`);
|
|
1125
|
+
runtime.lastRenderedBlock = 'status';
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
case 'delegation': {
|
|
1131
|
+
stopSpinner();
|
|
1132
|
+
clearPendingHead();
|
|
1133
|
+
const from = data?.from || '';
|
|
1134
|
+
const to = data?.to || '';
|
|
1135
|
+
session.delegations.push({ from, to, time: Date.now() });
|
|
1136
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1137
|
+
process.stderr.write(` ${c.brand('↳')} ${c.dim(from)} ${c.brand('→')} ${c.bold(to)}`);
|
|
1138
|
+
if (data?.instruction) {
|
|
1139
|
+
process.stderr.write(` ${c.dim(data.instruction.slice(0, 50))}`);
|
|
1140
|
+
}
|
|
1141
|
+
process.stderr.write('\n');
|
|
1142
|
+
runtime.lastRenderedBlock = 'status';
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ── Sub-Agent Activity ──
|
|
1147
|
+
|
|
1148
|
+
case 'sub_agent_start': {
|
|
1149
|
+
stopSpinner();
|
|
1150
|
+
clearPendingHead();
|
|
1151
|
+
const agentType = data?.type || 'sub-agent';
|
|
1152
|
+
const query = data?.query || '';
|
|
1153
|
+
renderBlockBoundary('subagent');
|
|
1154
|
+
process.stderr.write(renderSubAgentOpen({ type: agentType, query }).replace(/^\n/, '') + '\n');
|
|
1155
|
+
runtime.lastRenderedBlock = 'subagent';
|
|
1156
|
+
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
1157
|
+
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
1158
|
+
startSpinner(`${agentType}: working...`);
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
case 'sub_agent_tool': {
|
|
1163
|
+
// The regular tool_call event renders the card, indented by the
|
|
1164
|
+
// sub-agent stack depth. Just update the spinner text here.
|
|
1165
|
+
const agentType = data?.type || 'sub-agent';
|
|
1166
|
+
const tool = data?.tool || '';
|
|
1167
|
+
if (!tool) break;
|
|
1168
|
+
// Don't clobber an active explore-run spinner. "exploring · 5 read ·
|
|
1169
|
+
// 2 searched" is more informative than "explore → search_code", and
|
|
1170
|
+
// sub_agent_tool fires on every step of a sub-agent — otherwise the
|
|
1171
|
+
// spinner would flip-flop between the two texts and read as blank.
|
|
1172
|
+
if (runtime.exploreRun.lineActive && isExploreTool(tool)) break;
|
|
1173
|
+
updateSpinner(`${agentType} → ${tool}`);
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
case 'sub_agent_complete': {
|
|
1178
|
+
stopSpinner();
|
|
1179
|
+
clearPendingHead();
|
|
1180
|
+
const agentType = data?.type || 'sub-agent';
|
|
1181
|
+
const usage = data?.usage || {};
|
|
1182
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
1183
|
+
const costUsd = usage.cost_usd ?? usage.total_cost_usd ?? data?.cost_usd ?? null;
|
|
1184
|
+
if (typeof costUsd === 'number') session.savedUsd += costUsd;
|
|
1185
|
+
const summary = data?.result_summary
|
|
1186
|
+
|| (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
|
|
1187
|
+
process.stderr.write(renderSubAgentClose({
|
|
1188
|
+
type: agentType,
|
|
1189
|
+
success: data?.success !== false,
|
|
1190
|
+
summary,
|
|
1191
|
+
costUsd,
|
|
1192
|
+
tokens,
|
|
1193
|
+
durationS: data?.duration_s,
|
|
1194
|
+
toolCalls: data?.tool_calls,
|
|
1195
|
+
iterations: data?.iterations,
|
|
1196
|
+
error: data?.error,
|
|
1197
|
+
}) + '\n');
|
|
1198
|
+
runtime.lastRenderedBlock = 'subagent';
|
|
1199
|
+
session.inSubAgent = inSubAgentBlock();
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
case 'plan_created': {
|
|
1204
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1205
|
+
process.stderr.write(` ${c.dim('project plan prepared')}\n`);
|
|
1206
|
+
runtime.lastRenderedBlock = 'status';
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
case 'goal_created': {
|
|
1211
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1212
|
+
process.stderr.write(` ${c.dim('project goal prepared')}\n`);
|
|
1213
|
+
runtime.lastRenderedBlock = 'status';
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
case 'session_info': {
|
|
1218
|
+
if (data?.session_id) {
|
|
1219
|
+
session.id = data.session_id;
|
|
1220
|
+
// Track in session manager so conversations save to the right file
|
|
1221
|
+
if (sessionMgrRef.current) sessionMgrRef.current.setSessionInfo({ session_id: data.session_id });
|
|
1222
|
+
}
|
|
1223
|
+
if (data?.model) session.model = data.model;
|
|
1224
|
+
if (data?.models?.coder) session.model = data.models.coder;
|
|
1225
|
+
if (data?.model_limits && typeof data.model_limits === 'object') {
|
|
1226
|
+
session.modelLimits = data.model_limits;
|
|
1227
|
+
}
|
|
1228
|
+
if (data?.user) session.user = { ...session.user, ...data.user };
|
|
1229
|
+
// BYOK users pay their model provider directly; the platform does not
|
|
1230
|
+
// charge them credits. Hide cost + credits when this flag is set.
|
|
1231
|
+
if (typeof data?.is_byok === 'boolean') session.isByok = data.is_byok;
|
|
1232
|
+
// Subscription tier + credit balance — backend is authoritative.
|
|
1233
|
+
if (data?.subscription_tier) session.subscriptionTier = data.subscription_tier;
|
|
1234
|
+
if (typeof data?.credits_included_limit === 'number') session.creditsLimit = data.credits_included_limit;
|
|
1235
|
+
const bal = data?.credits_balance;
|
|
1236
|
+
if (bal && typeof bal === 'object') {
|
|
1237
|
+
if (typeof bal.total === 'number') session.creditsTotal = bal.total;
|
|
1238
|
+
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
1239
|
+
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
1240
|
+
}
|
|
1241
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
case 'error':
|
|
1246
|
+
stopSpinner();
|
|
1247
|
+
flushContent();
|
|
1248
|
+
{
|
|
1249
|
+
const guidance = formatAgentErrorGuidance(data || {});
|
|
1250
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1251
|
+
process.stderr.write(` ${c.red('✗')} ${guidance.title}\n`);
|
|
1252
|
+
for (const line of guidance.lines) {
|
|
1253
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
1254
|
+
}
|
|
1255
|
+
if (guidance.meta.length) {
|
|
1256
|
+
process.stderr.write(` ${c.dim(guidance.meta.join(' · '))}\n`);
|
|
1257
|
+
}
|
|
1258
|
+
runtime.lastRenderedBlock = 'status';
|
|
1259
|
+
}
|
|
1260
|
+
break;
|
|
1261
|
+
|
|
1262
|
+
case 'complete': {
|
|
1263
|
+
stopSpinner();
|
|
1264
|
+
flushContent();
|
|
1265
|
+
resetSubAgents();
|
|
1266
|
+
session.inSubAgent = false;
|
|
1267
|
+
|
|
1268
|
+
const summary = data?.summary || '';
|
|
1269
|
+
if (summary && !runtime.renderedContentThisTurn) {
|
|
1270
|
+
renderBlockBoundary('content');
|
|
1271
|
+
if (!runtime.contentHeaderPrinted) {
|
|
1272
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
1273
|
+
runtime.contentHeaderPrinted = true;
|
|
1274
|
+
}
|
|
1275
|
+
const rendered = renderMarkdown(summary);
|
|
1276
|
+
for (const line of rendered.split('\n')) {
|
|
1277
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
1278
|
+
}
|
|
1279
|
+
runtime.renderedContentThisTurn = true;
|
|
1280
|
+
runtime.lastRenderedBlock = 'content';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Update session token counts
|
|
1284
|
+
const usage = data?.usage;
|
|
1285
|
+
let turnCost = 0;
|
|
1286
|
+
if (usage) {
|
|
1287
|
+
const inp = usage.total_input_tokens || usage.input_tokens || 0;
|
|
1288
|
+
const out = usage.total_output_tokens || usage.output_tokens || 0;
|
|
1289
|
+
session.inputTokens += inp;
|
|
1290
|
+
session.outputTokens += out;
|
|
1291
|
+
|
|
1292
|
+
// Model-aware cost calculation
|
|
1293
|
+
const costResult = calculateCost(usage);
|
|
1294
|
+
turnCost = costResult.total;
|
|
1295
|
+
session.totalCost += costResult.total;
|
|
1296
|
+
session.costAccurate = costResult.accurate;
|
|
1297
|
+
|
|
1298
|
+
// Accumulate per-model breakdown
|
|
1299
|
+
for (const entry of costResult.breakdown) {
|
|
1300
|
+
const existing = session.costBreakdown.find(b => b.model === entry.model);
|
|
1301
|
+
if (existing) {
|
|
1302
|
+
existing.input_tokens += entry.input_tokens;
|
|
1303
|
+
existing.output_tokens += entry.output_tokens;
|
|
1304
|
+
existing.cache_read_tokens += entry.cache_read_tokens || 0;
|
|
1305
|
+
existing.cache_creation_tokens += entry.cache_creation_tokens || 0;
|
|
1306
|
+
existing.cost += entry.cost;
|
|
1307
|
+
} else {
|
|
1308
|
+
session.costBreakdown.push({ ...entry });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
session.lastTurnDuration = data?.duration_s || 0;
|
|
1314
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
1315
|
+
|
|
1316
|
+
// ── Server-authoritative credits ──
|
|
1317
|
+
// Backend sends usage.credits_charged (this turn) + balance (remaining)
|
|
1318
|
+
// in the complete event. CLI uses these instead of the local
|
|
1319
|
+
// costToCredits estimate so /status and /cost match the dashboard.
|
|
1320
|
+
if (!session.isByok) {
|
|
1321
|
+
const msgStatus = lowWindowStatus(session.rateLimit);
|
|
1322
|
+
if (!session.msgsLowWarned && msgStatus !== 'ok') {
|
|
1323
|
+
const windowLine = formatMessageWindow(session.rateLimit);
|
|
1324
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1325
|
+
if (msgStatus === 'exhausted') {
|
|
1326
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim(`${windowLine}. Wait for the window to reset or upgrade at bahulam.ai/pricing.`)}\n`);
|
|
1327
|
+
} else {
|
|
1328
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`${windowLine}. Message window is running low.`)}\n`);
|
|
1329
|
+
}
|
|
1330
|
+
runtime.lastRenderedBlock = 'status';
|
|
1331
|
+
session.msgsLowWarned = true;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const charged = data?.usage?.credits_charged;
|
|
1335
|
+
if (typeof charged === 'number') session.creditsCharged += charged;
|
|
1336
|
+
const bal = data?.balance;
|
|
1337
|
+
if (bal && typeof bal === 'object') {
|
|
1338
|
+
if (typeof bal.total === 'number') session.creditsTotal = bal.total;
|
|
1339
|
+
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
1340
|
+
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
1341
|
+
}
|
|
1342
|
+
// Warn once per turn when the remaining credits drop below 20% of the
|
|
1343
|
+
// tier's included limit (or below 10 absolute for tiny tiers). Credits
|
|
1344
|
+
// stay out of the always-on prompt strip; this warning is the exception.
|
|
1345
|
+
if (!session.creditsLowWarned && typeof session.creditsTotal === 'number' && session.creditsLimit) {
|
|
1346
|
+
const threshold = Math.max(10, Math.floor(session.creditsLimit * 0.2));
|
|
1347
|
+
if (session.creditsTotal <= threshold && session.creditsTotal > 0) {
|
|
1348
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1349
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade or top up at bahulam.ai/pricing.`)}\n`);
|
|
1350
|
+
runtime.lastRenderedBlock = 'status';
|
|
1351
|
+
session.creditsLowWarned = true;
|
|
1352
|
+
} else if (session.creditsTotal <= 0) {
|
|
1353
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1354
|
+
process.stderr.write(` ${c.red('✗')} ${c.yellow(`Credit balance exhausted on the ${session.subscriptionTier || 'free'} plan. Purchase credits at bahulam.ai/pricing or switch to BYOK.`)}\n`);
|
|
1355
|
+
runtime.lastRenderedBlock = 'status';
|
|
1356
|
+
session.creditsLowWarned = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Sync cumulative session cost into the orbit (status bar shows it).
|
|
1362
|
+
if (orbitRef.current) orbitRef.current.onCost(session.totalCost);
|
|
1363
|
+
|
|
1364
|
+
// Compact turn summary. Backend's tool_calls is authoritative and
|
|
1365
|
+
// includes primary + sub-agent internals for billing/credit rollups.
|
|
1366
|
+
const observedPrimaryTools = session.toolCalls;
|
|
1367
|
+
const observedSubAgentTools = session.subAgentToolCalls;
|
|
1368
|
+
const observedTurnTools = observedPrimaryTools + observedSubAgentTools;
|
|
1369
|
+
if (Number.isFinite(data?.primary_tool_calls)) {
|
|
1370
|
+
session.toolCalls = data.primary_tool_calls;
|
|
1371
|
+
const delta = data.primary_tool_calls - observedPrimaryTools;
|
|
1372
|
+
if (delta > 0) session.totalPrimaryToolCalls += delta;
|
|
1373
|
+
}
|
|
1374
|
+
if (Number.isFinite(data?.sub_agent_tool_calls)) {
|
|
1375
|
+
session.subAgentToolCalls = data.sub_agent_tool_calls;
|
|
1376
|
+
const delta = data.sub_agent_tool_calls - observedSubAgentTools;
|
|
1377
|
+
if (delta > 0) session.totalSubAgentToolCalls += delta;
|
|
1378
|
+
}
|
|
1379
|
+
if (Number.isFinite(data?.tool_calls)) {
|
|
1380
|
+
const delta = data.tool_calls - observedTurnTools;
|
|
1381
|
+
if (delta > 0) session.totalToolCalls += delta;
|
|
1382
|
+
}
|
|
1383
|
+
const tools = Number.isFinite(data?.tool_calls)
|
|
1384
|
+
? data.tool_calls
|
|
1385
|
+
: (session.toolCalls + session.subAgentToolCalls);
|
|
1386
|
+
|
|
1387
|
+
// Mission report — replaces the trailing "Done" when the turn did real
|
|
1388
|
+
// work (touched files or invoked tools). Plain chat turns keep the
|
|
1389
|
+
// tight printTurnSummary so the report does not feel ceremonial.
|
|
1390
|
+
const didRealWork = tools > 0 || session.filesChanged.length > 0;
|
|
1391
|
+
if (didRealWork) {
|
|
1392
|
+
const successOverall = data?.success !== false;
|
|
1393
|
+
const report = renderMissionReport({
|
|
1394
|
+
task: session.lastTask,
|
|
1395
|
+
success: successOverall,
|
|
1396
|
+
filesChanged: session.filesChanged,
|
|
1397
|
+
filesRead: session.filesRead,
|
|
1398
|
+
toolCounts: session.toolCounts,
|
|
1399
|
+
subAgents: { ...session.subAgentCounts, savedUsd: 0 },
|
|
1400
|
+
costUsd: null,
|
|
1401
|
+
durationS: data?.duration_s,
|
|
1402
|
+
testsPass: data?.tests_passed != null
|
|
1403
|
+
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
1404
|
+
: null,
|
|
1405
|
+
blockers: !successOverall ? (data?.blockers || extractBlockers(data)) : null,
|
|
1406
|
+
nextActions: [],
|
|
1407
|
+
cwd: safeCwd(),
|
|
1408
|
+
});
|
|
1409
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1410
|
+
process.stderr.write(report.replace(/^\n/, '') + '\n');
|
|
1411
|
+
runtime.lastRenderedBlock = 'status';
|
|
1412
|
+
} else {
|
|
1413
|
+
printTurnSummary(tools, data?.duration_s, turnCost);
|
|
1414
|
+
}
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
case 'cancelled':
|
|
1419
|
+
stopSpinner();
|
|
1420
|
+
flushContent();
|
|
1421
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1422
|
+
process.stderr.write(` ${c.yellow('⏹')} Cancelled${data?.reason ? ': ' + c.dim(data.reason) : ''}\n`);
|
|
1423
|
+
runtime.lastRenderedBlock = 'status';
|
|
1424
|
+
break;
|
|
1425
|
+
|
|
1426
|
+
case 'paused':
|
|
1427
|
+
stopSpinner();
|
|
1428
|
+
flushPendingHead();
|
|
1429
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1430
|
+
process.stderr.write(` ${c.yellow('⏸')} Paused${data?.reason ? ' ' + c.dim(data.reason) : ''}\n`);
|
|
1431
|
+
runtime.lastRenderedBlock = 'status';
|
|
1432
|
+
break;
|
|
1433
|
+
|
|
1434
|
+
case 'resumed':
|
|
1435
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1436
|
+
process.stderr.write(` ${c.green('▶')} Resumed\n`);
|
|
1437
|
+
runtime.lastRenderedBlock = 'status';
|
|
1438
|
+
break;
|
|
1439
|
+
|
|
1440
|
+
default:
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ── Slash Commands ──
|
|
1446
|
+
|
|
1447
|
+
function taskListLabel(list) {
|
|
1448
|
+
return {
|
|
1449
|
+
active: 'Active',
|
|
1450
|
+
backlog: 'Backlog',
|
|
1451
|
+
blocked: 'Blocked',
|
|
1452
|
+
done: 'Done',
|
|
1453
|
+
}[list] || list;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function firstMeaningfulLines(content, limit = 6) {
|
|
1457
|
+
return String(content || '')
|
|
1458
|
+
.split(/\r?\n/)
|
|
1459
|
+
.map(line => line.trim())
|
|
1460
|
+
.filter(line => line && !line.startsWith('#'))
|
|
1461
|
+
.slice(0, limit);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function renderTaskBoard(board, { showDone = false } = {}) {
|
|
1465
|
+
const order = showDone
|
|
1466
|
+
? ['active', 'blocked', 'backlog', 'done']
|
|
1467
|
+
: ['active', 'blocked', 'backlog'];
|
|
1468
|
+
let any = false;
|
|
1469
|
+
for (const list of order) {
|
|
1470
|
+
const tasks = board.lists[list]?.tasks || [];
|
|
1471
|
+
if (!tasks.length) continue;
|
|
1472
|
+
any = true;
|
|
1473
|
+
process.stderr.write(`\n ${c.bold(taskListLabel(list))} ${c.dim(board.lists[list].fileName)}\n`);
|
|
1474
|
+
for (const task of tasks.slice(0, 12)) {
|
|
1475
|
+
const marker = task.checked ? c.green('[x]') : c.dim('[ ]');
|
|
1476
|
+
const section = task.section && task.section !== taskListLabel(list) ? c.dim(` · ${task.section}`) : '';
|
|
1477
|
+
process.stderr.write(` ${marker} ${task.text}${section}\n`);
|
|
1478
|
+
}
|
|
1479
|
+
if (tasks.length > 12) {
|
|
1480
|
+
process.stderr.write(` ${c.dim(`+${tasks.length - 12} more`)}\n`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (!any) {
|
|
1484
|
+
process.stderr.write(` ${c.dim('No project tasks yet. Add one with /tasks add <text>.')}\n`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
|
|
1489
|
+
const cwd = safeCwd();
|
|
1490
|
+
ensureTaskFiles({ cwd });
|
|
1491
|
+
const board = loadTaskBoard({ cwd });
|
|
1492
|
+
const counts = taskCounts(board);
|
|
1493
|
+
const planLines = firstMeaningfulLines(board.plan.content, 8);
|
|
1494
|
+
const goalLines = firstMeaningfulLines(board.goal.content, 3);
|
|
1495
|
+
const effective = ctx.effectivePolicy || loadEffectivePolicy({ cwd });
|
|
1496
|
+
const owner = effective.policy?.planning?.owner || 'auto';
|
|
1497
|
+
|
|
1498
|
+
process.stderr.write(`\n ${c.bold('Plan')}\n`);
|
|
1499
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1500
|
+
process.stderr.write(` ${c.dim('Owner')} ${c.brand(owner)}\n`);
|
|
1501
|
+
process.stderr.write(` ${c.dim('Tasks')} ${counts.active} active, ${counts.blocked} blocked, ${counts.backlog} backlog, ${counts.done} done\n`);
|
|
1502
|
+
if (mode === 'status') {
|
|
1503
|
+
process.stderr.write(` ${c.dim('Plan file')} ${board.plan.exists ? board.plan.path : c.dim('(none)')}\n`);
|
|
1504
|
+
process.stderr.write(` ${c.dim('Tasks dir')} ${board.dir}\n`);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (goalLines.length) {
|
|
1508
|
+
process.stderr.write(`\n ${c.bold('Goal')}\n`);
|
|
1509
|
+
for (const line of goalLines) process.stderr.write(` ${line}\n`);
|
|
1510
|
+
}
|
|
1511
|
+
if (planLines.length) {
|
|
1512
|
+
process.stderr.write(`\n ${c.bold('Current Plan')}\n`);
|
|
1513
|
+
for (const line of planLines) process.stderr.write(` ${line}\n`);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
renderTaskBoard(board, { showDone: mode === 'status' });
|
|
1517
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function refreshTaskContext(ctx) {
|
|
1521
|
+
try {
|
|
1522
|
+
const previous = ctx.latestProjectContext || null;
|
|
1523
|
+
ctx.latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous });
|
|
1524
|
+
ctx.latestEnvelope = null;
|
|
1525
|
+
} catch { /* best effort */ }
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function handleTasksCommand(rest, ctx) {
|
|
1529
|
+
const raw = String(rest || '').trim();
|
|
1530
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
1531
|
+
if (!raw || raw === 'list') {
|
|
1532
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
1533
|
+
process.stderr.write(`\n ${c.bold('Tasks')}\n`);
|
|
1534
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1535
|
+
renderTaskBoard(board, { showDone: true });
|
|
1536
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (raw === 'help') {
|
|
1541
|
+
renderHelp('plan');
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const parts = raw.split(/\s+/);
|
|
1546
|
+
let verb = (parts.shift() || '').toLowerCase();
|
|
1547
|
+
|
|
1548
|
+
if (verb === 'move') {
|
|
1549
|
+
try {
|
|
1550
|
+
const [from, index, to, ...textParts] = parts;
|
|
1551
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
1552
|
+
refreshTaskContext(ctx);
|
|
1553
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1556
|
+
process.stderr.write(` ${c.gray('Usage: /tasks move <active|backlog|blocked|done> <number> <active|backlog|blocked|done> [new text]')}\n`);
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (verb === 'edit' || verb === 'rename') {
|
|
1562
|
+
try {
|
|
1563
|
+
const [list, index, ...textParts] = parts;
|
|
1564
|
+
const result = updateTask({ cwd: safeCwd(), list, index, text: textParts.join(' ') });
|
|
1565
|
+
refreshTaskContext(ctx);
|
|
1566
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`updated ${result.list} #${result.index}`)} ${result.text}\n`);
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1569
|
+
process.stderr.write(` ${c.gray('Usage: /tasks edit <active|backlog|blocked|done> <number> <new text>')}\n`);
|
|
1570
|
+
}
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (verb === 'remove' || verb === 'rm' || verb === 'delete') {
|
|
1575
|
+
try {
|
|
1576
|
+
const [list, index] = parts;
|
|
1577
|
+
const result = removeTask({ cwd: safeCwd(), list, index });
|
|
1578
|
+
refreshTaskContext(ctx);
|
|
1579
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`removed ${result.list} #${result.index}`)} ${result.task.text}\n`);
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1582
|
+
process.stderr.write(` ${c.gray('Usage: /tasks remove <active|backlog|blocked|done> <number>')}\n`);
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (verb === 'finish' || verb === 'complete' || verb === 'block' || verb === 'unblock') {
|
|
1588
|
+
try {
|
|
1589
|
+
const [from, index, ...textParts] = parts;
|
|
1590
|
+
const to = verb === 'block' ? 'blocked' : verb === 'unblock' ? 'active' : 'done';
|
|
1591
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
1592
|
+
refreshTaskContext(ctx);
|
|
1593
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1596
|
+
process.stderr.write(` ${c.gray(`Usage: /tasks ${verb} <active|backlog|blocked|done> <number> [new text]`)}\n`);
|
|
1597
|
+
}
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
let list = 'backlog';
|
|
1602
|
+
if (verb === 'add' || verb === 'new') {
|
|
1603
|
+
const maybeList = (parts[0] || '').toLowerCase();
|
|
1604
|
+
if (maybeList in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(maybeList)) {
|
|
1605
|
+
list = parts.shift();
|
|
1606
|
+
}
|
|
1607
|
+
} else if (verb in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(verb)) {
|
|
1608
|
+
list = verb;
|
|
1609
|
+
} else {
|
|
1610
|
+
parts.unshift(verb);
|
|
1611
|
+
verb = 'add';
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
try {
|
|
1615
|
+
const result = appendTask({ cwd: safeCwd(), list, text: parts.join(' ') });
|
|
1616
|
+
refreshTaskContext(ctx);
|
|
1617
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`added to ${result.list}`)} ${result.text}\n`);
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function handleCommand(input, ctx) {
|
|
1624
|
+
const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
|
|
1625
|
+
if (aliasTarget) {
|
|
1626
|
+
process.stderr.write(` ${c.dim(`Legacy alias: use ${aliasTarget}`)}\n`);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
switch (cmd) {
|
|
1630
|
+
case '/help': {
|
|
1631
|
+
renderHelp(rest);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
case '/plan': {
|
|
1636
|
+
const mode = rest.trim().toLowerCase();
|
|
1637
|
+
if (mode === 'help') {
|
|
1638
|
+
renderHelp('plan');
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (mode === 'edit') {
|
|
1642
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
1643
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
1644
|
+
process.stderr.write(`\n ${c.bold('Editable Plan Files')}\n`);
|
|
1645
|
+
process.stderr.write(` ${c.dim('Plan')} ${board.plan.path}\n`);
|
|
1646
|
+
process.stderr.write(` ${c.dim('Active')} ${board.lists.active.path}\n`);
|
|
1647
|
+
process.stderr.write(` ${c.dim('Backlog')} ${board.lists.backlog.path}\n`);
|
|
1648
|
+
process.stderr.write(` ${c.dim('Blocked')} ${board.lists.blocked.path}\n`);
|
|
1649
|
+
process.stderr.write(` ${c.dim('Done')} ${board.lists.done.path}\n\n`);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
renderPlanOverview({ ctx, mode: mode === 'status' ? 'status' : 'overview' });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
case '/tasks':
|
|
1657
|
+
handleTasksCommand(rest, ctx);
|
|
1658
|
+
return;
|
|
1659
|
+
|
|
1660
|
+
case '/attach':
|
|
1661
|
+
handleAttachCommand(rest, ctx);
|
|
1662
|
+
return;
|
|
1663
|
+
|
|
1664
|
+
case '/attachments':
|
|
1665
|
+
handleAttachmentsCommand(rest, ctx);
|
|
1666
|
+
return;
|
|
1667
|
+
|
|
1668
|
+
case '/history': {
|
|
1669
|
+
if (rest.trim() === 'fold') {
|
|
1670
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /history last or d to expand.')}\n`);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (rest.trim() === 'help') {
|
|
1674
|
+
renderHelp('history');
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (rest.trim() === 'approvals') {
|
|
1678
|
+
const entries = ctx.approval?.approvalLog?.readRecent?.(20) || [];
|
|
1679
|
+
if (!entries.length) {
|
|
1680
|
+
process.stderr.write(` ${c.gray('No approval log entries yet.')}\n`);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
process.stderr.write(`\n ${c.bold('Approval History')}\n`);
|
|
1684
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
1685
|
+
for (const e of entries) {
|
|
1686
|
+
const when = e.ts ? String(e.ts).slice(0, 19).replace('T', ' ') : '';
|
|
1687
|
+
const decision = e.decision?.includes('reject') || e.decision?.includes('deny') ? c.red(e.decision) : c.green(e.decision);
|
|
1688
|
+
process.stderr.write(` ${c.dim(when)} ${decision} ${c.brand(e.tool || '?')} ${c.dim(e.scope || 'once')} ${c.dim(e.args || '')}\n`);
|
|
1689
|
+
}
|
|
1690
|
+
process.stderr.write('\n');
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
1694
|
+
renderHistoryEntries(session.history, { limit: 20, maxChars: 120 });
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
case '/login':
|
|
1699
|
+
process.stderr.write(`${c.brand('Starting login flow...')}\n`);
|
|
1700
|
+
try {
|
|
1701
|
+
await ctx.auth.login();
|
|
1702
|
+
process.stderr.write(`${c.green('✓ Login successful!')}\n`);
|
|
1703
|
+
await fetchUser(ctx);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
process.stderr.write(`${c.red('✗ Login failed: ' + err.message)}\n`);
|
|
1706
|
+
}
|
|
1707
|
+
return;
|
|
1708
|
+
|
|
1709
|
+
case '/whoami': {
|
|
1710
|
+
if (!session.user) await fetchUser(ctx);
|
|
1711
|
+
if (session.user) {
|
|
1712
|
+
process.stderr.write(`\n ${c.green('✓')} ${session.user.github_username}\n`);
|
|
1713
|
+
process.stderr.write(` ${c.gray('Email:')} ${session.user.email || 'n/a'}\n`);
|
|
1714
|
+
process.stderr.write(` ${c.gray('User ID:')} ${session.user.id}\n`);
|
|
1715
|
+
process.stderr.write(` ${c.gray('Role:')} ${session.user.role || 'user'}\n\n`);
|
|
1716
|
+
} else {
|
|
1717
|
+
process.stderr.write(` ${c.red('Not logged in. Run /login.')}\n`);
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
case '/status': {
|
|
1723
|
+
if (rest.trim() === 'help') {
|
|
1724
|
+
renderHelp('status');
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (rest.trim() === 'context') {
|
|
1728
|
+
const current = ctx.latestProjectContext || loadProjectContext({ cwd: safeCwd() });
|
|
1729
|
+
const envelope = ctx.latestEnvelope || buildContextEnvelope({
|
|
1730
|
+
cwd: safeCwd(),
|
|
1731
|
+
effectivePolicy: ctx.effectivePolicy,
|
|
1732
|
+
projectContext: current,
|
|
1733
|
+
projectResources: ctx.toolExecutor?.getProjectResources?.() || [],
|
|
1734
|
+
agentContext: ctx.toolExecutor?.getAgentContext?.() || {},
|
|
1735
|
+
});
|
|
1736
|
+
process.stderr.write(`\n ${c.bold('Context')}\n`);
|
|
1737
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1738
|
+
const loaded = current.loaded || [];
|
|
1739
|
+
if (!loaded.length) {
|
|
1740
|
+
process.stderr.write(` ${c.dim('No .bahulam context files loaded yet.')}\n`);
|
|
1741
|
+
} else {
|
|
1742
|
+
for (const file of loaded) {
|
|
1743
|
+
const changed = file.changed ? ` ${c.yellow('updated')}` : '';
|
|
1744
|
+
process.stderr.write(` ${c.brand(file.label.padEnd(18))} ${c.dim(file.hash)} ${c.dim(file.path)}${changed}\n`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
process.stderr.write(`\n ${c.bold('Command Context')}\n`);
|
|
1748
|
+
process.stderr.write(` ${c.dim('Active')} ${envelope.command_context.active_command || c.dim('(none)')}\n`);
|
|
1749
|
+
process.stderr.write(` ${c.dim('Source')} ${envelope.command_context.source}\n`);
|
|
1750
|
+
process.stderr.write(` ${c.dim('Timeout')} ${envelope.command_context.runtime_limits.command_timeout_seconds}s command, ${envelope.command_context.runtime_limits.tool_timeout_seconds}s tool\n`);
|
|
1751
|
+
process.stderr.write(` ${c.dim('Plan owner')} ${envelope.effective_options.plan_owner}\n`);
|
|
1752
|
+
process.stderr.write(` ${c.dim('HITL scope')} ${envelope.effective_options.hitl_default_scope} ${c.dim(`(reask ${envelope.effective_options.reask_after_minutes}m)`)}\n`);
|
|
1753
|
+
if (envelope.available_skills.length) {
|
|
1754
|
+
process.stderr.write(`\n ${c.bold('Skills')}\n`);
|
|
1755
|
+
for (const skill of envelope.available_skills.slice(0, 12)) {
|
|
1756
|
+
process.stderr.write(` ${c.brand(skill.name)} ${c.dim(skill.scope)} ${c.dim(skill.description || '')}\n`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
process.stderr.write('\n');
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const creds = ctx.auth.loadCredentials();
|
|
1764
|
+
const env = process.env.TARANG_ENV || 'production';
|
|
1765
|
+
const os = await import('node:os');
|
|
1766
|
+
const mem = process.memoryUsage();
|
|
1767
|
+
const approvalSummary = ctx.approval.getSummary();
|
|
1768
|
+
|
|
1769
|
+
process.stderr.write(`\n ${c.bold('Session')}\n`);
|
|
1770
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
1771
|
+
process.stderr.write(` ${c.dim('ID')} ${session.id || c.dim('(not assigned yet)')}\n`);
|
|
1772
|
+
process.stderr.write(` ${c.dim('User')} ${session.user?.github_username || '—'}\n`);
|
|
1773
|
+
process.stderr.write(` ${c.dim('Model')} ${session.model || 'backend default'}\n`);
|
|
1774
|
+
const modelOverrides = sessionModelOverrideEntries();
|
|
1775
|
+
if (modelOverrides.length) {
|
|
1776
|
+
const summary = modelOverrides
|
|
1777
|
+
.slice(0, 4)
|
|
1778
|
+
.map(([role, model]) => `${MODEL_ROLE_LABELS[role] || role}=${model}`)
|
|
1779
|
+
.join(', ');
|
|
1780
|
+
const more = modelOverrides.length > 4 ? ` +${modelOverrides.length - 4} more` : '';
|
|
1781
|
+
process.stderr.write(` ${c.dim('Overrides')} ${summary}${more}\n`);
|
|
1782
|
+
}
|
|
1783
|
+
if (env === 'local') {
|
|
1784
|
+
process.stderr.write(` ${c.dim('Backend')} ${creds.backendUrl}\n`);
|
|
1785
|
+
}
|
|
1786
|
+
process.stderr.write(` ${c.dim('Env')} ${env}\n`);
|
|
1787
|
+
process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
|
|
1788
|
+
const toolSplit = session.totalSubAgentToolCalls > 0
|
|
1789
|
+
? ` ${c.dim(`(${session.totalPrimaryToolCalls} primary, ${session.totalSubAgentToolCalls} sub-agent)`)}`
|
|
1790
|
+
: '';
|
|
1791
|
+
const lastTurnSplit = session.subAgentToolCalls > 0
|
|
1792
|
+
? ` ${c.dim(`(${session.toolCalls} primary, ${session.subAgentToolCalls} sub-agent)`)}`
|
|
1793
|
+
: '';
|
|
1794
|
+
process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total${toolSplit}, ${session.toolCalls + session.subAgentToolCalls} last turn${lastTurnSplit}\n`);
|
|
1795
|
+
process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
|
|
1796
|
+
if (session.isByok) {
|
|
1797
|
+
process.stderr.write(` ${c.dim('Billing')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
1798
|
+
} else {
|
|
1799
|
+
// Server-authoritative remaining balance; fall back to the per-session
|
|
1800
|
+
// charged tally when balance hasn't been pushed yet.
|
|
1801
|
+
if (session.subscriptionTier) {
|
|
1802
|
+
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())}\n`);
|
|
1803
|
+
}
|
|
1804
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
1805
|
+
if (messageWindow) {
|
|
1806
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
1807
|
+
}
|
|
1808
|
+
if (typeof session.creditsTotal === 'number') {
|
|
1809
|
+
const limit = session.creditsLimit ? ` ${c.dim('/ ' + formatCredits(session.creditsLimit))}` : '';
|
|
1810
|
+
const used = session.creditsCharged ? ` ${c.dim(`(${formatCredits(session.creditsCharged)} used this session)`)}` : '';
|
|
1811
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(session.creditsTotal)}${limit}${used}\n`);
|
|
1812
|
+
} else if (session.creditsCharged) {
|
|
1813
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(session.creditsCharged)} ${c.dim('(used this session)')}\n`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
1817
|
+
|
|
1818
|
+
// Cache — PRD-071 §1.2. Only surface when we have data; a fresh session
|
|
1819
|
+
// shows nothing rather than a misleading "0%".
|
|
1820
|
+
const cache = computeCacheTotals();
|
|
1821
|
+
if (cache.read > 0 || cache.write > 0) {
|
|
1822
|
+
const readLabel = formatTokens(cache.read);
|
|
1823
|
+
const writeLabel = formatTokens(cache.write);
|
|
1824
|
+
process.stderr.write(` ${c.dim('Cache')} ${cache.hitRate}% hit ${c.dim('·')} ${readLabel} read ${c.dim('·')} ${writeLabel} write\n`);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Permissions
|
|
1828
|
+
process.stderr.write(`\n ${c.bold('Permissions')}\n`);
|
|
1829
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
1830
|
+
process.stderr.write(` ${c.dim('Approved')} ${approvalSummary.approved} ${c.dim('Denied')} ${approvalSummary.denied}\n`);
|
|
1831
|
+
if (approvalSummary.autoApproveAll) {
|
|
1832
|
+
process.stderr.write(` ${c.dim('Mode')} ${c.yellow('approve-all active')}\n`);
|
|
1833
|
+
}
|
|
1834
|
+
if (approvalSummary.autoApprovedTypes.length > 0) {
|
|
1835
|
+
process.stderr.write(` ${c.dim('Auto-types')} ${approvalSummary.autoApprovedTypes.join(', ')}\n`);
|
|
1836
|
+
}
|
|
1837
|
+
if (approvalSummary.trust) {
|
|
1838
|
+
process.stderr.write(` ${c.dim('Trust')} ${approvalSummary.trust.sessionRules} session, ${approvalSummary.trust.projectRules} project rules\n`);
|
|
1839
|
+
}
|
|
1840
|
+
process.stderr.write(` ${c.dim('Blocked')} ${session.blockedOps} by safety guardrails\n`);
|
|
1841
|
+
|
|
1842
|
+
// Orchestration
|
|
1843
|
+
if (session.delegations.length > 0 || session.phases.length > 0) {
|
|
1844
|
+
process.stderr.write(`\n ${c.bold('Orchestration')}\n`);
|
|
1845
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
1846
|
+
if (session.delegations.length > 0) {
|
|
1847
|
+
process.stderr.write(` ${c.dim('Delegations')} ${session.delegations.length}\n`);
|
|
1848
|
+
for (const d of session.delegations.slice(-5)) {
|
|
1849
|
+
process.stderr.write(` ${c.dim(d.from)} ${c.brand('→')} ${d.to}\n`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (session.phases.length > 0) {
|
|
1853
|
+
process.stderr.write(` ${c.dim('Phases')} ${session.phases.map(p => p.name).join(' → ')}\n`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Files changed
|
|
1858
|
+
if (session.filesChanged.length > 0) {
|
|
1859
|
+
process.stderr.write(`\n ${c.bold('Files Changed')} ${c.dim(`(${session.filesChanged.length})`)}\n`);
|
|
1860
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
1861
|
+
for (const f of session.filesChanged.slice(-10)) {
|
|
1862
|
+
process.stderr.write(` ${c.dim('~')} ${f}\n`);
|
|
1863
|
+
}
|
|
1864
|
+
if (session.filesChanged.length > 10) {
|
|
1865
|
+
process.stderr.write(` ${c.dim(` ...and ${session.filesChanged.length - 10} more`)}\n`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// System
|
|
1870
|
+
process.stderr.write(`\n ${c.bold('System')}\n`);
|
|
1871
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
1872
|
+
process.stderr.write(` ${c.dim('Node')} ${process.version}\n`);
|
|
1873
|
+
process.stderr.write(` ${c.dim('Platform')} ${process.platform} ${os.arch()}\n`);
|
|
1874
|
+
process.stderr.write(` ${c.dim('Heap')} ${(mem.heapUsed / 1024 / 1024).toFixed(0)} MB\n`);
|
|
1875
|
+
process.stderr.write(` ${c.dim('Memory')} ${((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024).toFixed(1)}G / ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}G\n\n`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
case '/settings': {
|
|
1880
|
+
const sub = rest.trim() || 'policy';
|
|
1881
|
+
if (sub === 'help') {
|
|
1882
|
+
renderHelp('settings');
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (sub !== 'policy') {
|
|
1886
|
+
process.stderr.write(` ${c.gray('Usage: /settings policy or /help settings')}\n`);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const effective = loadEffectivePolicy({ cwd: safeCwd() });
|
|
1890
|
+
ctx.effectivePolicy = effective;
|
|
1891
|
+
const rows = formatPolicySourceRows(effective);
|
|
1892
|
+
process.stderr.write(`\n ${c.bold('Effective Policy')}\n`);
|
|
1893
|
+
process.stderr.write(` ${c.dim('─'.repeat(86))}\n`);
|
|
1894
|
+
for (const row of rows.slice(0, 80)) {
|
|
1895
|
+
const value = typeof row.value === 'string' ? row.value : JSON.stringify(row.value);
|
|
1896
|
+
process.stderr.write(` ${c.brand(row.key.padEnd(38))} ${c.dim(row.source.padEnd(8))} ${String(value).slice(0, 34)}\n`);
|
|
1897
|
+
}
|
|
1898
|
+
if (rows.length > 80) process.stderr.write(` ${c.dim(`...and ${rows.length - 80} more`)}\n`);
|
|
1899
|
+
const projectLayer = effective.layers.find(l => l.name === 'project');
|
|
1900
|
+
if (projectLayer?.error) {
|
|
1901
|
+
process.stderr.write(`\n ${c.yellow('Project config error:')} ${projectLayer.error}\n`);
|
|
1902
|
+
}
|
|
1903
|
+
process.stderr.write('\n');
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
case '/stats': {
|
|
1908
|
+
const os = await import('node:os');
|
|
1909
|
+
const mem = process.memoryUsage();
|
|
1910
|
+
const totalMem = os.totalmem();
|
|
1911
|
+
const usedMem = totalMem - os.freemem();
|
|
1912
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
1913
|
+
const ctxPct = Math.min(100, (totalTokens / 200000) * 100);
|
|
1914
|
+
|
|
1915
|
+
process.stderr.write(`\n ${c.bold('Metrics')}\n`);
|
|
1916
|
+
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
1917
|
+
process.stderr.write(` ${progressBar(ctxPct, 15, 'Context')} ${(totalTokens / 1000).toFixed(1)}k tok\n`);
|
|
1918
|
+
process.stderr.write(` ${progressBar(Math.round((usedMem / totalMem) * 100), 15, 'Memory')} ${(usedMem / 1024 / 1024 / 1024).toFixed(1)}G\n`);
|
|
1919
|
+
process.stderr.write(` ${progressBar(Math.round((mem.heapUsed / mem.heapTotal) * 100), 15, 'Heap')} ${(mem.heapUsed / 1024 / 1024).toFixed(0)}M\n`);
|
|
1920
|
+
process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
|
|
1921
|
+
process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls + session.subAgentToolCalls}`);
|
|
1922
|
+
if (session.subAgentToolCalls > 0) {
|
|
1923
|
+
process.stderr.write(c.dim(` (${session.toolCalls} primary, ${session.subAgentToolCalls} sub-agent)`));
|
|
1924
|
+
}
|
|
1925
|
+
process.stderr.write('\n');
|
|
1926
|
+
process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
|
|
1927
|
+
if (session.isByok) {
|
|
1928
|
+
process.stderr.write(` ${c.gray('Billing:')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
1929
|
+
} else {
|
|
1930
|
+
process.stderr.write(` ${c.gray('Credits:')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
1931
|
+
}
|
|
1932
|
+
process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
case '/cost': {
|
|
1937
|
+
if (session.isByok) {
|
|
1938
|
+
process.stderr.write(`\n ${c.bold('Billing')} ${c.green('BYOK')} ${c.dim('— you pay your model provider directly. Bahulam does not charge credits for BYOK usage.')}\n\n`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
// Prefer server-authoritative numbers when available.
|
|
1942
|
+
const used = session.creditsCharged || 0;
|
|
1943
|
+
const usedLabel = formatCredits(used);
|
|
1944
|
+
process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(usedLabel)}`);
|
|
1945
|
+
if (used > 0 && !session.creditsCharged) process.stderr.write(` ${c.yellow('(estimated)')}`);
|
|
1946
|
+
process.stderr.write('\n');
|
|
1947
|
+
if (session.subscriptionTier && typeof session.creditsTotal === 'number') {
|
|
1948
|
+
const remaining = formatCredits(session.creditsTotal);
|
|
1949
|
+
const limit = session.creditsLimit ? ` / ${formatCredits(session.creditsLimit)}` : '';
|
|
1950
|
+
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())} ${c.dim('· remaining')} ${c.brand(remaining)}${c.dim(limit)}\n`);
|
|
1951
|
+
}
|
|
1952
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
1953
|
+
if (messageWindow) {
|
|
1954
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
1955
|
+
}
|
|
1956
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1957
|
+
|
|
1958
|
+
if (session.costBreakdown.length > 0) {
|
|
1959
|
+
// Header
|
|
1960
|
+
process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('Credits'.padStart(10))}\n`);
|
|
1961
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1962
|
+
|
|
1963
|
+
for (const b of session.costBreakdown) {
|
|
1964
|
+
const modelLabel = b.model === 'unknown' ? c.yellow('unknown model') : b.model;
|
|
1965
|
+
const roleTag = b.role && b.role !== 'unknown' ? ` ${c.dim(`(${b.role})`)}` : '';
|
|
1966
|
+
const cacheTokens = (b.cache_read_tokens || 0) + (b.cache_creation_tokens || 0);
|
|
1967
|
+
const costStr = b.free ? c.green('free') : formatCredits(costToCredits(b.cost));
|
|
1968
|
+
|
|
1969
|
+
process.stderr.write(
|
|
1970
|
+
` ${(modelLabel + roleTag).padEnd(36)}` +
|
|
1971
|
+
`${formatTokens(b.input_tokens).padStart(10)}` +
|
|
1972
|
+
`${formatTokens(b.output_tokens).padStart(10)}` +
|
|
1973
|
+
`${(cacheTokens > 0 ? formatTokens(cacheTokens) : '—').padStart(10)}` +
|
|
1974
|
+
`${costStr.padStart(10)}\n`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
process.stderr.write(
|
|
1982
|
+
` ${c.bold('Total'.padEnd(36))}` +
|
|
1983
|
+
`${formatTokens(session.inputTokens).padStart(10)}` +
|
|
1984
|
+
`${formatTokens(session.outputTokens).padStart(10)}` +
|
|
1985
|
+
`${''.padStart(10)}` +
|
|
1986
|
+
`${formatCredits(costToCredits(session.totalCost)).padStart(10)}\n`
|
|
1987
|
+
);
|
|
1988
|
+
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
case '/last':
|
|
1993
|
+
expandLast();
|
|
1994
|
+
return;
|
|
1995
|
+
|
|
1996
|
+
case '/expand': {
|
|
1997
|
+
const arg = rest.trim();
|
|
1998
|
+
if (!arg) { expandLast(); return; }
|
|
1999
|
+
if (arg === 'all') { expandIndex('all'); return; }
|
|
2000
|
+
const n = Number(arg);
|
|
2001
|
+
if (!Number.isFinite(n)) {
|
|
2002
|
+
process.stderr.write(` ${c.gray('Usage: /expand [n|all] — n is the 1-based index from the start of the session')}\n`);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
// Users pass 1-based; getCard accepts negative (-1 = last) or positive index.
|
|
2006
|
+
expandIndex(n > 0 ? n - 1 : n);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
case '/fold':
|
|
2011
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /last or d to expand.')}\n`);
|
|
2012
|
+
return;
|
|
2013
|
+
|
|
2014
|
+
case '/undo': {
|
|
2015
|
+
const result = ctx.checkpoints?.undo();
|
|
2016
|
+
if (!result) {
|
|
2017
|
+
process.stderr.write(` ${c.gray('No checkpoints to undo.')}\n`);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (result.restored) {
|
|
2021
|
+
process.stderr.write(` ${c.green('↩')} ${c.dim('Restored')} ${result.filePath}\n`);
|
|
2022
|
+
} else {
|
|
2023
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim('Undo failed: ' + (result.error || 'unknown error'))}\n`);
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
case '/checkpoint': {
|
|
2029
|
+
const list = ctx.checkpoints?.list(10) || [];
|
|
2030
|
+
if (!list.length) {
|
|
2031
|
+
process.stderr.write(` ${c.gray('No checkpoints recorded yet — they are taken automatically before each edit.')}\n`);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
process.stderr.write(`\n ${c.bold('Recent checkpoints')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
2035
|
+
for (const ckpt of list) {
|
|
2036
|
+
const when = String(ckpt.timestamp).slice(11, 19);
|
|
2037
|
+
process.stderr.write(` ${c.gray(when)} ${c.white(ckpt.file)} ${c.gray(formatTokens(ckpt.size) + ' bytes')}\n`);
|
|
2038
|
+
}
|
|
2039
|
+
process.stderr.write(`\n ${c.gray('/undo restores the most recent one')}\n\n`);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
case '/preflight': {
|
|
2044
|
+
await runPreflight({ auth: ctx.auth, cwd: safeCwd(), version: VERSION });
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
case '/report': {
|
|
2049
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0 && session.filesRead.length === 0) {
|
|
2050
|
+
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const state = {
|
|
2054
|
+
task: session.lastTask,
|
|
2055
|
+
success: true,
|
|
2056
|
+
filesChanged: session.filesChanged,
|
|
2057
|
+
filesRead: session.filesRead,
|
|
2058
|
+
toolCounts: session.toolCounts,
|
|
2059
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
2060
|
+
costUsd: session.isByok ? null : session.totalCost,
|
|
2061
|
+
durationS: (Date.now() - session.startTime) / 1000,
|
|
2062
|
+
nextActions: [],
|
|
2063
|
+
cwd: safeCwd(),
|
|
2064
|
+
};
|
|
2065
|
+
const out = saveReport(state, { cwd: safeCwd() });
|
|
2066
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
case '/why': {
|
|
2071
|
+
if (!session.lastReasoning) {
|
|
2072
|
+
process.stderr.write(` ${c.gray('No reasoning captured yet for this session.')}\n`);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
process.stderr.write(`\n ${c.bold('Last reasoning')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
2076
|
+
for (const line of String(session.lastReasoning).split('\n')) {
|
|
2077
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
2078
|
+
}
|
|
2079
|
+
process.stderr.write('\n');
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
case '/map': {
|
|
2084
|
+
try {
|
|
2085
|
+
const resources = ctx.toolExecutor?.getProjectResources?.() || [];
|
|
2086
|
+
if (!resources.length) {
|
|
2087
|
+
process.stderr.write(` ${c.gray('No project resources registered yet. Use get_project_overview to register one.')}\n`);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
process.stderr.write(`\n ${c.bold('Registered projects')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
2091
|
+
for (const r of resources) {
|
|
2092
|
+
process.stderr.write(` ${c.brand('•')} ${c.white(r.id || r.name || '?')} ${c.dim(r.root || r.path || '')}\n`);
|
|
2093
|
+
}
|
|
2094
|
+
process.stderr.write('\n');
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
process.stderr.write(` ${c.red('/map failed: ' + err.message)}\n`);
|
|
2097
|
+
}
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
case '/budget': {
|
|
2102
|
+
const arg = rest.trim();
|
|
2103
|
+
if (!arg) {
|
|
2104
|
+
const current = session.budgetUsd ? `$${session.budgetUsd.toFixed(2)}` : 'not set';
|
|
2105
|
+
process.stderr.write(` ${c.dim('Budget cap:')} ${c.brand(current)} ${c.dim('· set with /status budget <amount> or clear with /status budget clear')}\n`);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (arg === 'clear' || arg === 'off') {
|
|
2109
|
+
session.budgetUsd = null;
|
|
2110
|
+
session.budgetExceeded = false;
|
|
2111
|
+
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
const n = Number(arg.replace(/^\$/, ''));
|
|
2115
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2116
|
+
process.stderr.write(` ${c.gray('Usage: /budget <amount in USD> or /budget clear')}\n`);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
session.budgetUsd = n;
|
|
2120
|
+
session.budgetExceeded = false;
|
|
2121
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Budget set: ')} $${n.toFixed(2)}\n`);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
case '/quiet':
|
|
2126
|
+
case '/verbose':
|
|
2127
|
+
case '/surgical': {
|
|
2128
|
+
const mode = cmd === '/quiet' ? V_MODES.QUIET
|
|
2129
|
+
: cmd === '/verbose' ? V_MODES.VERBOSE
|
|
2130
|
+
: V_MODES.SURGICAL;
|
|
2131
|
+
setVerbosity(mode);
|
|
2132
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Verbosity: ')} ${c.brand(verbosityLabel(mode))}\n`);
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
case '/compact': {
|
|
2137
|
+
await compactCurrentSession(ctx, rest);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
case '/model': {
|
|
2142
|
+
handleModelCommand(rest);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
case '/new':
|
|
2147
|
+
if (ctx.startNewSession) await ctx.startNewSession();
|
|
2148
|
+
else process.stderr.write(` ${c.yellow('!')} ${c.dim('New session reset is unavailable in this mode.')}\n`);
|
|
2149
|
+
return;
|
|
2150
|
+
|
|
2151
|
+
case '/clear':
|
|
2152
|
+
session.history.length = 0;
|
|
2153
|
+
session.agentHistory.length = 0;
|
|
2154
|
+
session.toolCalls = 0;
|
|
2155
|
+
session.subAgentToolCalls = 0;
|
|
2156
|
+
clearCards();
|
|
2157
|
+
process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
|
|
2158
|
+
return;
|
|
2159
|
+
|
|
2160
|
+
case '/git': {
|
|
2161
|
+
const { execSync } = await import('node:child_process');
|
|
2162
|
+
try { process.stdout.write(execSync('git status --short --branch', { encoding: 'utf-8' }) + '\n'); }
|
|
2163
|
+
catch (e) { process.stderr.write(` ${c.red(e.message)}\n`); }
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
case '/diff': {
|
|
2168
|
+
const { execSync } = await import('node:child_process');
|
|
2169
|
+
try {
|
|
2170
|
+
const diff = execSync('git diff --no-ext-diff --unified=3', {
|
|
2171
|
+
encoding: 'utf-8',
|
|
2172
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
2173
|
+
});
|
|
2174
|
+
process.stdout.write(diff ? renderDiff(diff) + '\n' : c.dim('(no changes)') + '\n');
|
|
2175
|
+
}
|
|
2176
|
+
catch (e) { process.stderr.write(` ${c.red(e.message)}\n`); }
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
case '/safety': {
|
|
2181
|
+
const { getSafetyRules } = await import('../core/safety.mjs');
|
|
2182
|
+
const rules = getSafetyRules();
|
|
2183
|
+
const summary = ctx.approval.getSummary();
|
|
2184
|
+
process.stderr.write(`\n ${c.bold('Safety Guardrails')} ${c.green('ACTIVE')}\n`);
|
|
2185
|
+
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
2186
|
+
process.stderr.write(` ${c.gray('Approval mode:')} ${ctx.approval.getModeLabel()}\n`);
|
|
2187
|
+
process.stderr.write(` ${c.gray('Approved:')} ${summary.approved} ${c.gray('Denied:')} ${summary.denied}\n`);
|
|
2188
|
+
process.stderr.write(` ${c.gray('Protected files:')} ${rules.protectedNames.join(', ')}\n`);
|
|
2189
|
+
process.stderr.write(` ${c.gray('Source dirs:')} ${rules.sourceDirs.join(', ')}\n`);
|
|
2190
|
+
process.stderr.write(` ${c.gray('Blocked patterns:')} ${rules.blockedPatterns}\n`);
|
|
2191
|
+
process.stderr.write(` ${c.gray('High-risk patterns:')} ${rules.highRiskPatterns}\n`);
|
|
2192
|
+
process.stderr.write(` ${c.gray('Ops blocked:')} ${session.blockedOps}\n\n`);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
case '/revoke': {
|
|
2197
|
+
const wasActive = ctx.approval.revoke();
|
|
2198
|
+
if (wasActive) {
|
|
2199
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Auto-approvals revoked. All tool calls will prompt again.')}\n`);
|
|
2200
|
+
} else {
|
|
2201
|
+
process.stderr.write(` ${c.gray('No auto-approvals were active.')}\n`);
|
|
2202
|
+
}
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
case '/sessions': {
|
|
2207
|
+
const resumable = await listResumableSessions();
|
|
2208
|
+
if (resumable.length === 0) {
|
|
2209
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
process.stderr.write(`\n ${c.bold('Resumable Sessions')}\n`);
|
|
2213
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
2214
|
+
for (const s of resumable) {
|
|
2215
|
+
const date = sessionListTimestamp(s);
|
|
2216
|
+
const instr = oneLineInstruction(s.instruction, 72);
|
|
2217
|
+
const project = s.project || path.basename(s.projectPath || '') || '(unknown)';
|
|
2218
|
+
process.stderr.write(` ${c.brand(s.sessionId)} ${c.brand(project)} ${c.dim(date)} ${messageCountLabel(s.messageCount)} ${c.dim(instr)}\n`);
|
|
2219
|
+
}
|
|
2220
|
+
process.stderr.write(`\n ${c.dim('Resume with:')} bahulam-code --resume <sessionId>\n`);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
case '/resume': {
|
|
2225
|
+
// PRD-068 §5.14: one-prompt picker, context-length driven auto-decision,
|
|
2226
|
+
// direct-resume mode flags (--full, --tail10, --tail20, --summary).
|
|
2227
|
+
const parts = input.split(/\s+/).filter(Boolean);
|
|
2228
|
+
const forcedFlag = parts.find(p => /^(--full|--tail10|--tail20|--recap|--summary|-f|-1|-2|-r|-s)$/.test(p));
|
|
2229
|
+
const forcedMode = forcedFlag
|
|
2230
|
+
? ({ '--full': 'full', '-f': 'full',
|
|
2231
|
+
'--tail10': 'tail-10', '-1': 'tail-10',
|
|
2232
|
+
'--tail20': 'tail-20', '-2': 'tail-20',
|
|
2233
|
+
'--recap': 'tail-20', '-r': 'tail-20',
|
|
2234
|
+
'--summary': 'summary', '-s': 'summary' })[forcedFlag]
|
|
2235
|
+
: null;
|
|
2236
|
+
const targetId = parts.slice(1).find(p => !p.startsWith('-'));
|
|
2237
|
+
|
|
2238
|
+
const resumable = await listResumableSessions();
|
|
2239
|
+
if (resumable.length === 0) {
|
|
2240
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// 1. Resolve which session to resume.
|
|
2245
|
+
let picked = null;
|
|
2246
|
+
if (targetId) {
|
|
2247
|
+
picked = resumable.find(s => s.sessionId === targetId || s.sessionId?.startsWith(targetId));
|
|
2248
|
+
if (!picked) {
|
|
2249
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`No session found matching id: ${targetId}`)}\n`);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
} else {
|
|
2253
|
+
while (!picked) {
|
|
2254
|
+
const pickResult = await pickResumableSession(resumable, ctx);
|
|
2255
|
+
if (!pickResult) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2256
|
+
if (pickResult.action === 'resume') {
|
|
2257
|
+
if (!pickResult.session) {
|
|
2258
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim('Empty session — pick another.')}\n`);
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
picked = pickResult.session;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
if (pickResult.action === 'preview') {
|
|
2265
|
+
// Yield to the event loop so any queued keystrokes from the picker
|
|
2266
|
+
// don't spill into the preview's stdin listener (PRD-068 §5.14 bugfix).
|
|
2267
|
+
await new Promise(r => setImmediate(r));
|
|
2268
|
+
const previewResult = await previewResumeSession(pickResult.session, ctx);
|
|
2269
|
+
if (previewResult && previewResult.action === 'resume') {
|
|
2270
|
+
picked = pickResult.session;
|
|
2271
|
+
// Preview already committed to a mode — skip the threshold overlay.
|
|
2272
|
+
picked._presetMode = previewResult.mode;
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
if (previewResult === null) {
|
|
2276
|
+
// getSessionDetail failed — file missing, unreadable, or huge.
|
|
2277
|
+
// Tell the user why before looping back to the picker.
|
|
2278
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('Could not load transcript for preview — pick another session or press Enter to resume without preview.')}\n`);
|
|
2279
|
+
}
|
|
2280
|
+
// Yield again so the loop-back render doesn't collide with the
|
|
2281
|
+
// just-closed preview's stdin cleanup.
|
|
2282
|
+
await new Promise(r => setImmediate(r));
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// 2. Decide the mode. Force-flag > preview-preset > auto-decide > overlay.
|
|
2288
|
+
let mode = forcedMode || picked._presetMode || null;
|
|
2289
|
+
if (!mode) {
|
|
2290
|
+
const currentModel = picked.modelLimits?.coder?.model
|
|
2291
|
+
|| picked.models?.[picked.models.length - 1]
|
|
2292
|
+
|| session?.model
|
|
2293
|
+
|| session?.user?.default_reasoning_model
|
|
2294
|
+
|| null;
|
|
2295
|
+
const decision = decideResumeMode({
|
|
2296
|
+
transcriptTokens: picked.contextTokens,
|
|
2297
|
+
model: currentModel,
|
|
2298
|
+
contextWindow: picked.modelLimits?.coder || session?.modelLimits?.coder || null,
|
|
2299
|
+
settings: ctx.effectivePolicy?.policy?.resume ? { resume: ctx.effectivePolicy.policy.resume } : {},
|
|
2300
|
+
});
|
|
2301
|
+
decision.resumeSummary = picked.resumeSummary || null;
|
|
2302
|
+
if (decision.mode === 'full') {
|
|
2303
|
+
mode = 'full';
|
|
2304
|
+
} else {
|
|
2305
|
+
// Yield before attaching the overlay listener — same race guard as
|
|
2306
|
+
// the preview branch above. Prevents a queued Enter from the picker
|
|
2307
|
+
// slipping into the overlay's stdin, which otherwise looked like a
|
|
2308
|
+
// duplicate picker render.
|
|
2309
|
+
await new Promise(r => setImmediate(r));
|
|
2310
|
+
const chosen = await chooseThresholdMode(ctx, decision);
|
|
2311
|
+
if (!chosen) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2312
|
+
mode = chosen;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// 3. Activate.
|
|
2317
|
+
const source = targetId ? 'direct' : 'picker';
|
|
2318
|
+
const progress = startResumeProgress(mode);
|
|
2319
|
+
let resumed;
|
|
2320
|
+
try {
|
|
2321
|
+
resumed = await ctx.activateResumedSession(picked.sessionId, source, mode, picked, {
|
|
2322
|
+
onProgress: progress.update,
|
|
2323
|
+
onProgressStop: progress.stop,
|
|
2324
|
+
});
|
|
2325
|
+
} finally {
|
|
2326
|
+
progress.stop();
|
|
2327
|
+
}
|
|
2328
|
+
if (!resumed.ok) {
|
|
2329
|
+
if (resumed.reason === 'cwd-cancelled') {
|
|
2330
|
+
process.stderr.write(`\n ${c.dim('Cancelled.')}\n`);
|
|
2331
|
+
} else {
|
|
2332
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim(resumed.reason || 'No messages in that session.')}\n`);
|
|
2333
|
+
}
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// 4. Report — succinct honest single line, then optional hydration warning.
|
|
2338
|
+
const toolSummary = resumed.stats && resumed.stats.toolCalls
|
|
2339
|
+
? `${resumed.stats.toolCalls} tool calls`
|
|
2340
|
+
: `${resumed.messages} msgs`;
|
|
2341
|
+
const summaryLabel = mode !== 'full' && resumed.summarySource
|
|
2342
|
+
? ` · summary: ${resumed.summarySource}`
|
|
2343
|
+
: '';
|
|
2344
|
+
process.stderr.write(`\n ${c.green('↺')} ${c.dim('Resumed')} ${c.brand(picked.project || path.basename(safeCwd()))} ${c.dim(`· ${resumed.messages} msgs · ${toolSummary} · mode: ${resumeModeLabel(mode)}${summaryLabel}`)}\n`);
|
|
2345
|
+
if (resumed.summaryWarning) {
|
|
2346
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — using local summary (${resumed.summaryWarning})`)}\n`);
|
|
2347
|
+
}
|
|
2348
|
+
if (resumed.hydrationFailures?.length) {
|
|
2349
|
+
for (const failure of resumed.hydrationFailures) {
|
|
2350
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`could not re-read project root: ${failure}`)}\n`);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
if (resumed.stayedInCwd) {
|
|
2354
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`resumed transcript from ${resumed.savedProjectPath} — running against ${safeCwd()}`)}\n`);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// 5. Show continuity context. Non-summary modes use the captured
|
|
2358
|
+
// kepler_event stream when available so the terminal replay matches
|
|
2359
|
+
// the original styled interaction; older sessions fall back to
|
|
2360
|
+
// reconstructed text.
|
|
2361
|
+
if (mode === 'summary' && resumed.summary) {
|
|
2362
|
+
// In summary mode the agent gets only the summary block. Show it so
|
|
2363
|
+
// the user knows what continuity context was included.
|
|
2364
|
+
process.stderr.write(`\n ${c.bold('Continuity Summary')}\n`);
|
|
2365
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
2366
|
+
for (const line of resumed.summary.split('\n')) {
|
|
2367
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
2368
|
+
}
|
|
2369
|
+
process.stderr.write('\n');
|
|
2370
|
+
} else if (resumed.replayEvents?.length) {
|
|
2371
|
+
renderResumePreview(resumed, { renderEvent });
|
|
2372
|
+
} else if (resumed.history?.length) {
|
|
2373
|
+
// Full/tail modes feed real conversation to the agent — show
|
|
2374
|
+
// the tail so the user has visual context. Cap at 30 entries to avoid
|
|
2375
|
+
// flooding the terminal on long sessions.
|
|
2376
|
+
renderHistoryEntries(resumed.history, {
|
|
2377
|
+
limit: 30,
|
|
2378
|
+
maxChars: 200,
|
|
2379
|
+
title: mode?.startsWith('tail-') ? `Recent turns (${mode.replace('tail-', 'last ')})` : 'Conversation history (last 30 entries)',
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
case '/agents':
|
|
2386
|
+
await handleAgentsCommand(rest, ctx);
|
|
2387
|
+
return;
|
|
2388
|
+
|
|
2389
|
+
case '/explore':
|
|
2390
|
+
case '/review':
|
|
2391
|
+
case '/architect': {
|
|
2392
|
+
if (!rest) {
|
|
2393
|
+
process.stderr.write(` ${c.yellow('Usage:')} ${cmd} <instruction>\n`);
|
|
2394
|
+
process.stderr.write(` ${c.gray(`Example: ${cmd} ${cmd === '/explore' ? 'how does authentication work?' : cmd === '/review' ? 'check src/core/ for bugs' : 'design a caching layer'}`)}\n`);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
return await runAgent(cmd.slice(1), rest, ctx, session, renderEvent);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
case '/logout': {
|
|
2401
|
+
const success = ctx.auth.logout();
|
|
2402
|
+
if (success) {
|
|
2403
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Signed out. Credentials cleared from ~/.bahulam/config.json')}\n`);
|
|
2404
|
+
process.stderr.write(` ${c.dim('Run /login to sign in again.')}\n`);
|
|
2405
|
+
} else {
|
|
2406
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No credentials to clear.')}\n`);
|
|
2407
|
+
}
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
case '/exit':
|
|
2412
|
+
case '/quit':
|
|
2413
|
+
if (isInputDockMounted()) unmountInputDock();
|
|
2414
|
+
process.stderr.write(`\n ${c.brand('Goodbye!')}\n\n`);
|
|
2415
|
+
process.exit(0);
|
|
2416
|
+
|
|
2417
|
+
default:
|
|
2418
|
+
process.stderr.write(` ${c.gray(`Unknown: ${cmd}. Type /help.`)}\n`);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// ── Fetch User Profile ──
|
|
2423
|
+
|
|
2424
|
+
async function fetchUser(ctx) {
|
|
2425
|
+
const creds = ctx.auth.loadCredentials();
|
|
2426
|
+
if (!creds.token) return;
|
|
2427
|
+
try {
|
|
2428
|
+
const resp = await fetch(`${creds.backendUrl}/api/user/me`, {
|
|
2429
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
2430
|
+
});
|
|
2431
|
+
if (resp.ok) {
|
|
2432
|
+
session.user = await resp.json();
|
|
2433
|
+
session.model = session.user.default_reasoning_model || session.user.default_orchestrator_model || null;
|
|
2434
|
+
}
|
|
2435
|
+
} catch {}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// ── Main REPL ──
|
|
2439
|
+
// Cache CWD at startup so safeCwd() has a fallback if the dir gets deleted
|
|
2440
|
+
|
|
2441
|
+
export async function startTerminalRepl() {
|
|
2442
|
+
safeCwd(); // prime the cache in repl-utils.mjs for later recovery
|
|
2443
|
+
|
|
2444
|
+
const cliArgs = parseArgs(process.argv.slice(2));
|
|
2445
|
+
const auth = new TarangAuth();
|
|
2446
|
+
|
|
2447
|
+
// Projects are registered and indexed on demand through get_project_overview.
|
|
2448
|
+
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
2449
|
+
let checkpoints = new CheckpointManager(safeCwd());
|
|
2450
|
+
let effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2451
|
+
let latestProjectContext = null;
|
|
2452
|
+
let latestEnvelope = null;
|
|
2453
|
+
let hookRunner = new HookRunner({ cwd: safeCwd() });
|
|
2454
|
+
let toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
2455
|
+
const skipPerms = cliArgs.freeswim;
|
|
2456
|
+
let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
2457
|
+
|
|
2458
|
+
// Session manager — persists conversation messages to .bahulam/conversations/
|
|
2459
|
+
let sessionMgr = new SessionManager(safeCwd());
|
|
2460
|
+
sessionMgrRef.current = sessionMgr; // expose to renderEvent
|
|
2461
|
+
|
|
2462
|
+
// Local JSONL writer — writes cc-lens compatible session data to ~/.bahulam/
|
|
2463
|
+
let jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
2464
|
+
|
|
2465
|
+
// Persistent stream client — session_id captured from backend on first turn
|
|
2466
|
+
let streamClient = null;
|
|
2467
|
+
|
|
2468
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
|
|
2469
|
+
|
|
2470
|
+
async function startNewSession({ announce = true } = {}) {
|
|
2471
|
+
stopSpinner();
|
|
2472
|
+
flushContent();
|
|
2473
|
+
flushPendingHead();
|
|
2474
|
+
flushCompactReadRun();
|
|
2475
|
+
clearCards();
|
|
2476
|
+
|
|
2477
|
+
const preserved = {
|
|
2478
|
+
inputHistory: session.inputHistory,
|
|
2479
|
+
user: session.user,
|
|
2480
|
+
model: session.model,
|
|
2481
|
+
modelLimits: session.modelLimits,
|
|
2482
|
+
modelOverrides: session.modelOverrides,
|
|
2483
|
+
isByok: session.isByok,
|
|
2484
|
+
subscriptionTier: session.subscriptionTier,
|
|
2485
|
+
creditsTotal: session.creditsTotal,
|
|
2486
|
+
creditsIncluded: session.creditsIncluded,
|
|
2487
|
+
creditsPurchased: session.creditsPurchased,
|
|
2488
|
+
creditsLimit: session.creditsLimit,
|
|
2489
|
+
rateLimit: session.rateLimit,
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2492
|
+
try { await jsonlWriter.close(); } catch {}
|
|
2493
|
+
|
|
2494
|
+
checkpoints = new CheckpointManager(safeCwd());
|
|
2495
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2496
|
+
hookRunner = new HookRunner({ cwd: safeCwd() });
|
|
2497
|
+
toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
2498
|
+
approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
2499
|
+
if (ctx._rl) approval.setReadline(ctx._rl);
|
|
2500
|
+
sessionMgr = new SessionManager(safeCwd());
|
|
2501
|
+
sessionMgrRef.current = sessionMgr;
|
|
2502
|
+
jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
2503
|
+
streamClient = null;
|
|
2504
|
+
latestProjectContext = null;
|
|
2505
|
+
latestEnvelope = null;
|
|
2506
|
+
|
|
2507
|
+
Object.assign(session, {
|
|
2508
|
+
id: null,
|
|
2509
|
+
startTime: Date.now(),
|
|
2510
|
+
inputTokens: 0,
|
|
2511
|
+
outputTokens: 0,
|
|
2512
|
+
toolCalls: 0,
|
|
2513
|
+
subAgentToolCalls: 0,
|
|
2514
|
+
totalToolCalls: 0,
|
|
2515
|
+
totalPrimaryToolCalls: 0,
|
|
2516
|
+
totalSubAgentToolCalls: 0,
|
|
2517
|
+
turns: 0,
|
|
2518
|
+
history: [],
|
|
2519
|
+
agentHistory: [],
|
|
2520
|
+
inputHistory: preserved.inputHistory,
|
|
2521
|
+
user: preserved.user,
|
|
2522
|
+
model: preserved.model,
|
|
2523
|
+
modelLimits: preserved.modelLimits,
|
|
2524
|
+
modelOverrides: preserved.modelOverrides,
|
|
2525
|
+
blockedOps: 0,
|
|
2526
|
+
delegations: [],
|
|
2527
|
+
phases: [],
|
|
2528
|
+
inSubAgent: false,
|
|
2529
|
+
filesChanged: [],
|
|
2530
|
+
filesRead: [],
|
|
2531
|
+
lastTurnDuration: 0,
|
|
2532
|
+
toolCounts: {},
|
|
2533
|
+
subAgentCounts: {},
|
|
2534
|
+
savedUsd: 0,
|
|
2535
|
+
lastTask: '',
|
|
2536
|
+
lastReasoning: '',
|
|
2537
|
+
budgetUsd: null,
|
|
2538
|
+
budgetExceeded: false,
|
|
2539
|
+
costBreakdown: [],
|
|
2540
|
+
totalCost: 0,
|
|
2541
|
+
costAccurate: false,
|
|
2542
|
+
isByok: preserved.isByok,
|
|
2543
|
+
subscriptionTier: preserved.subscriptionTier,
|
|
2544
|
+
creditsTotal: preserved.creditsTotal,
|
|
2545
|
+
creditsIncluded: preserved.creditsIncluded,
|
|
2546
|
+
creditsPurchased: preserved.creditsPurchased,
|
|
2547
|
+
creditsLimit: preserved.creditsLimit,
|
|
2548
|
+
creditsCharged: 0,
|
|
2549
|
+
creditsLowWarned: false,
|
|
2550
|
+
rateLimit: preserved.rateLimit,
|
|
2551
|
+
msgsLowWarned: false,
|
|
2552
|
+
_lastEmittedThinking: '',
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
Object.assign(ctx, {
|
|
2556
|
+
toolExecutor,
|
|
2557
|
+
approval,
|
|
2558
|
+
jsonlWriter,
|
|
2559
|
+
sessionMgr,
|
|
2560
|
+
checkpoints,
|
|
2561
|
+
effectivePolicy,
|
|
2562
|
+
latestProjectContext,
|
|
2563
|
+
latestEnvelope,
|
|
2564
|
+
pendingVisionPaths: ctx.pendingVisionPaths || [],
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
if (announce) process.stderr.write(` ${c.green('✓')} ${c.dim('New session started.')}\n`);
|
|
2568
|
+
}
|
|
2569
|
+
ctx.startNewSession = startNewSession;
|
|
2570
|
+
|
|
2571
|
+
/**
|
|
2572
|
+
* Activate a previously-recorded session for continuation.
|
|
2573
|
+
*
|
|
2574
|
+
* Contract (PRD-068 §5.14 and follow-up clarification):
|
|
2575
|
+
* 1. Keep the same sessionId. The resumed session IS the same session,
|
|
2576
|
+
* not a fork. Future turns are appended to the SAME .jsonl file that
|
|
2577
|
+
* was read here.
|
|
2578
|
+
* 2. Do not re-write the loaded transcript back to disk. The file already
|
|
2579
|
+
* contains every historical entry; the load path is read-only. Any
|
|
2580
|
+
* duplication would double-count tokens on the next resume.
|
|
2581
|
+
* 3. Fresh sessions (kepler started without /resume) get a fresh UUID
|
|
2582
|
+
* the first time jsonlWriter.writeUserTurn() runs — that path is
|
|
2583
|
+
* untouched by resume, so brand-new sessions never inherit an old id.
|
|
2584
|
+
* 4. In-memory history (session.history / session.agentHistory) mirrors
|
|
2585
|
+
* what the agent will see next turn; it is NOT written back to the
|
|
2586
|
+
* transcript at activation time.
|
|
2587
|
+
*/
|
|
2588
|
+
async function activateResumedSession(sessionId, source = 'resume', historyMode = 'full', resumeEntry = null, options = {}) {
|
|
2589
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
2590
|
+
const onProgressStop = typeof options.onProgressStop === 'function' ? options.onProgressStop : () => {};
|
|
2591
|
+
// PRD-068 §5.14.6: JSONL is the only source. No legacy conversation fallback.
|
|
2592
|
+
onProgress('reading saved transcript', 14);
|
|
2593
|
+
const detail = await getSessionDetail(sessionId, { filePath: resumeEntry?.transcriptPath });
|
|
2594
|
+
if (!detail) {
|
|
2595
|
+
return { ok: false, reason: `No transcript found for session ${sessionId}` };
|
|
2596
|
+
}
|
|
2597
|
+
onProgress('building resume context', 28);
|
|
2598
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, historyMode);
|
|
2599
|
+
const displayHistory = richHistory.displayHistory;
|
|
2600
|
+
if (!displayHistory.length) {
|
|
2601
|
+
return { ok: false, reason: `Session ${sessionId} has no readable messages` };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// PRD-068 §5.14.7: explicit cwd confirmation if saved path differs.
|
|
2605
|
+
const savedProjectPath = detail?.meta?.project || '';
|
|
2606
|
+
let summarySource = 'local';
|
|
2607
|
+
let summaryWarning = '';
|
|
2608
|
+
if (historyMode !== 'full' && richHistory.sourceMessages?.length) {
|
|
2609
|
+
onProgress('summarizing transcript', 34);
|
|
2610
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
2611
|
+
auth,
|
|
2612
|
+
toolExecutor,
|
|
2613
|
+
sessionId,
|
|
2614
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
2615
|
+
messages: richHistory.sourceMessages,
|
|
2616
|
+
});
|
|
2617
|
+
if (backendSummary?.summary) {
|
|
2618
|
+
richHistory.summary = combineResumeSummaries(richHistory.priorSummary, backendSummary.summary);
|
|
2619
|
+
const summaryIndex = Number.isInteger(richHistory.summaryMessageIndex)
|
|
2620
|
+
? richHistory.summaryMessageIndex
|
|
2621
|
+
: 0;
|
|
2622
|
+
if (richHistory.agentHistory?.[summaryIndex]) {
|
|
2623
|
+
const tailTurns = resumeTailTurnCount(historyMode);
|
|
2624
|
+
const prefix = tailTurns
|
|
2625
|
+
? `Summary of earlier turns before the retained last ${tailTurns} conversation messages:\n`
|
|
2626
|
+
: 'Session continuity summary:\n';
|
|
2627
|
+
richHistory.agentHistory[summaryIndex] = {
|
|
2628
|
+
...richHistory.agentHistory[summaryIndex],
|
|
2629
|
+
content: `${prefix}${richHistory.summary}`,
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
summarySource = backendSummary.source || 'backend';
|
|
2633
|
+
} else {
|
|
2634
|
+
summarySource = 'local fallback';
|
|
2635
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
2636
|
+
}
|
|
2637
|
+
} else if (historyMode !== 'full') {
|
|
2638
|
+
summarySource = 'not needed';
|
|
2639
|
+
summaryWarning = resumeTailTurnCount(historyMode)
|
|
2640
|
+
? 'retained tail covers the whole transcript'
|
|
2641
|
+
: 'empty transcript';
|
|
2642
|
+
}
|
|
2643
|
+
const agentHistory = richHistory.agentHistory;
|
|
2644
|
+
const originalCwd = safeCwd();
|
|
2645
|
+
let switchedProject = false;
|
|
2646
|
+
let projectMissing = false;
|
|
2647
|
+
let stayedInCwd = false;
|
|
2648
|
+
onProgress('checking project cwd', 40);
|
|
2649
|
+
if (savedProjectPath && savedProjectPath !== originalCwd) {
|
|
2650
|
+
if (fs.existsSync(savedProjectPath)) {
|
|
2651
|
+
onProgressStop();
|
|
2652
|
+
const choice = await confirmCwdSwitch(ctx, savedProjectPath, originalCwd);
|
|
2653
|
+
if (choice === 'cancel') return { ok: false, reason: 'cwd-cancelled' };
|
|
2654
|
+
if (choice === 'switch') {
|
|
2655
|
+
try {
|
|
2656
|
+
process.chdir(savedProjectPath);
|
|
2657
|
+
safeCwd(); // re-prime the cache after chdir
|
|
2658
|
+
switchedProject = true;
|
|
2659
|
+
} catch {
|
|
2660
|
+
projectMissing = true;
|
|
2661
|
+
}
|
|
2662
|
+
} else if (choice === 'stay') {
|
|
2663
|
+
stayedInCwd = true;
|
|
2664
|
+
}
|
|
2665
|
+
} else {
|
|
2666
|
+
projectMissing = true;
|
|
2667
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`saved project path unavailable: ${savedProjectPath} — using current cwd`)}\n`);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
onProgress('rebuilding local session state', 55);
|
|
2672
|
+
checkpoints = new CheckpointManager(safeCwd());
|
|
2673
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2674
|
+
hookRunner = new HookRunner({ cwd: safeCwd(), sessionId });
|
|
2675
|
+
toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
2676
|
+
approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
2677
|
+
if (ctx._rl) approval.setReadline(ctx._rl);
|
|
2678
|
+
sessionMgr = new SessionManager(safeCwd());
|
|
2679
|
+
sessionMgr.activateSession(sessionId, {
|
|
2680
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
2681
|
+
started_at: detail?.meta?.startTime || new Date().toISOString(),
|
|
2682
|
+
}, displayHistory.filter(m => m.role === 'user' || m.role === 'assistant'));
|
|
2683
|
+
sessionMgrRef.current = sessionMgr;
|
|
2684
|
+
|
|
2685
|
+
try { await jsonlWriter.close(); } catch {}
|
|
2686
|
+
jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
2687
|
+
jsonlWriter.setSessionId(sessionId);
|
|
2688
|
+
if (
|
|
2689
|
+
historyMode !== 'full'
|
|
2690
|
+
&& richHistory.summary
|
|
2691
|
+
&& Number(richHistory.summaryCoveredMessageCount) > Number(richHistory.summaryCheckpointMessageCount || 0)
|
|
2692
|
+
) {
|
|
2693
|
+
jsonlWriter.writeKeplerEvent({
|
|
2694
|
+
type: 'resume_summary',
|
|
2695
|
+
data: {
|
|
2696
|
+
session_id: sessionId,
|
|
2697
|
+
mode: historyMode,
|
|
2698
|
+
mode_label: resumeModeLabel(historyMode),
|
|
2699
|
+
summary: richHistory.summary,
|
|
2700
|
+
summary_source: summarySource,
|
|
2701
|
+
summary_warning: summaryWarning || null,
|
|
2702
|
+
source_message_count: richHistory.summaryCoveredMessageCount,
|
|
2703
|
+
previous_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
2704
|
+
full_message_count: richHistory.fullMessageCount || 0,
|
|
2705
|
+
},
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
jsonlWriter.writeKeplerEvent({
|
|
2709
|
+
type: 'resume_context',
|
|
2710
|
+
data: {
|
|
2711
|
+
session_id: sessionId,
|
|
2712
|
+
source,
|
|
2713
|
+
mode: historyMode,
|
|
2714
|
+
mode_label: resumeModeLabel(historyMode),
|
|
2715
|
+
messages: displayHistory.length,
|
|
2716
|
+
summary_source: summarySource,
|
|
2717
|
+
summary_injected: historyMode !== 'full' && Boolean(richHistory.agentHistory?.[richHistory.summaryMessageIndex ?? 0]?.content),
|
|
2718
|
+
summary_warning: summaryWarning || null,
|
|
2719
|
+
summary_source_message_count: richHistory.summaryCoveredMessageCount || 0,
|
|
2720
|
+
previous_summary_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
2721
|
+
project_path: savedProjectPath || safeCwd(),
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
streamClient = null;
|
|
2726
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd() });
|
|
2727
|
+
latestEnvelope = null;
|
|
2728
|
+
|
|
2729
|
+
// PRD-068 §5.14.8: report hydration failures instead of swallowing them.
|
|
2730
|
+
const hydrationFailures = [];
|
|
2731
|
+
const resumeRoots = getTranscriptProjectRoots(detail);
|
|
2732
|
+
const rootsToRegister = [...new Set([safeCwd(), ...resumeRoots].filter(Boolean))];
|
|
2733
|
+
onProgress('hydrating project roots', 68);
|
|
2734
|
+
for (let i = 0; i < rootsToRegister.length; i++) {
|
|
2735
|
+
const root = rootsToRegister[i];
|
|
2736
|
+
onProgress(`hydrating project root ${i + 1}/${rootsToRegister.length}`, 68 + Math.round((i / Math.max(1, rootsToRegister.length)) * 18));
|
|
2737
|
+
try {
|
|
2738
|
+
await toolExecutor.execute('get_project_overview', { path: root });
|
|
2739
|
+
} catch {
|
|
2740
|
+
hydrationFailures.push(root);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
onProgress('preparing replay', 92);
|
|
2745
|
+
session.history = displayHistory;
|
|
2746
|
+
session.agentHistory = agentHistory;
|
|
2747
|
+
session.id = sessionId;
|
|
2748
|
+
session.turns = displayHistory.filter(m => m.role === 'user').length;
|
|
2749
|
+
session.lastTask = detail?.meta?.firstPrompt || session.history.find(m => m.role === 'user')?.content || '';
|
|
2750
|
+
|
|
2751
|
+
Object.assign(ctx, {
|
|
2752
|
+
toolExecutor,
|
|
2753
|
+
approval,
|
|
2754
|
+
jsonlWriter,
|
|
2755
|
+
sessionMgr,
|
|
2756
|
+
checkpoints,
|
|
2757
|
+
effectivePolicy,
|
|
2758
|
+
latestProjectContext,
|
|
2759
|
+
latestEnvelope,
|
|
2760
|
+
pendingVisionPaths: ctx.pendingVisionPaths || [],
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
return {
|
|
2764
|
+
ok: true,
|
|
2765
|
+
messages: displayHistory.length,
|
|
2766
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
2767
|
+
savedProjectPath,
|
|
2768
|
+
switchedProject,
|
|
2769
|
+
projectMissing,
|
|
2770
|
+
stayedInCwd,
|
|
2771
|
+
hydrationFailures,
|
|
2772
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
2773
|
+
historyMode,
|
|
2774
|
+
summary: richHistory.summary || '',
|
|
2775
|
+
summarySource,
|
|
2776
|
+
summaryWarning,
|
|
2777
|
+
history: displayHistory,
|
|
2778
|
+
replayEvents: detail.replayEvents || [],
|
|
2779
|
+
stats: richHistory.stats,
|
|
2780
|
+
source,
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
ctx.activateResumedSession = activateResumedSession;
|
|
2784
|
+
|
|
2785
|
+
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
2786
|
+
// The status bar shrinks the scroll region; if it mounts first, the
|
|
2787
|
+
// banner scrolls off-screen before the user ever sees it.
|
|
2788
|
+
printBanner(auth);
|
|
2789
|
+
|
|
2790
|
+
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
2791
|
+
// KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
2792
|
+
if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
|
|
2793
|
+
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
2794
|
+
catch { /* preflight is best-effort */ }
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// ── Initialization ──
|
|
2798
|
+
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
2799
|
+
await fetchUser(ctx);
|
|
2800
|
+
|
|
2801
|
+
// Clear the spinner line
|
|
2802
|
+
process.stderr.write(`\r${' '.repeat(60)}\r`);
|
|
2803
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
|
|
2804
|
+
if (session.user) {
|
|
2805
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
|
|
2806
|
+
}
|
|
2807
|
+
// ── Resume previous session ──
|
|
2808
|
+
if (cliArgs.resume) {
|
|
2809
|
+
const lastSession = cliArgs.resumeSessionId
|
|
2810
|
+
? { sessionId: cliArgs.resumeSessionId }
|
|
2811
|
+
: sessionMgr.getLastSession();
|
|
2812
|
+
|
|
2813
|
+
if (lastSession) {
|
|
2814
|
+
const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
|
|
2815
|
+
if (resumed.ok) {
|
|
2816
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
|
|
2817
|
+
process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
|
|
2818
|
+
process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
|
|
2819
|
+
if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
|
|
2820
|
+
if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
|
|
2821
|
+
if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
|
|
2822
|
+
process.stderr.write('\n');
|
|
2823
|
+
renderResumePreview(resumed, { renderEvent });
|
|
2824
|
+
} else {
|
|
2825
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
2826
|
+
}
|
|
2827
|
+
} else {
|
|
2828
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
|
|
2833
|
+
|
|
2834
|
+
// Keep one bottom-reserved UI surface: the fixed input dock. The older
|
|
2835
|
+
// status bar used the same terminal scroll-region primitive, so mounting
|
|
2836
|
+
// both would make prompt placement unpredictable.
|
|
2837
|
+
orbitRef.current = createOrbit();
|
|
2838
|
+
const inputDockActive = mountInputDock();
|
|
2839
|
+
if (inputDockActive) {
|
|
2840
|
+
process.on('beforeExit', unmountInputDock);
|
|
2841
|
+
process.on('exit', unmountInputDock);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// ── Bracketed paste (DEC private mode 2004) ──────────────────────────────
|
|
2845
|
+
// Ask the terminal to wrap pasted content in ESC[200~ … ESC[201~ markers so
|
|
2846
|
+
// we can merge a multi-line paste into a single input regardless of how
|
|
2847
|
+
// slowly the bytes arrive. Falls back to the legacy 35 ms debounce for
|
|
2848
|
+
// terminals that ignore the request.
|
|
2849
|
+
const PASTE_BEGIN = '\x1b[200~';
|
|
2850
|
+
const PASTE_END = '\x1b[201~';
|
|
2851
|
+
let _inBracketedPaste = false;
|
|
2852
|
+
let _bracketedPasteBuffer = '';
|
|
2853
|
+
const _pasteEndListeners = new Set();
|
|
2854
|
+
function onBracketedPasteEnd(cb) { _pasteEndListeners.add(cb); return () => _pasteEndListeners.delete(cb); }
|
|
2855
|
+
function isInBracketedPaste() { return _inBracketedPaste; }
|
|
2856
|
+
|
|
2857
|
+
if (process.stdin.isTTY) {
|
|
2858
|
+
try { process.stderr.write('\x1b[?2004h'); } catch {}
|
|
2859
|
+
const disableBracketedPaste = () => { try { process.stderr.write('\x1b[?2004l'); } catch {} };
|
|
2860
|
+
process.on('exit', disableBracketedPaste);
|
|
2861
|
+
process.once('SIGINT', disableBracketedPaste);
|
|
2862
|
+
process.once('SIGTERM', disableBracketedPaste);
|
|
2863
|
+
|
|
2864
|
+
// Prepend so we see raw bytes before readline consumes them.
|
|
2865
|
+
process.stdin.prependListener('data', (chunk) => {
|
|
2866
|
+
const s = chunk.toString('utf8');
|
|
2867
|
+
let i = 0;
|
|
2868
|
+
while (i < s.length) {
|
|
2869
|
+
if (!_inBracketedPaste) {
|
|
2870
|
+
const start = s.indexOf(PASTE_BEGIN, i);
|
|
2871
|
+
if (start === -1) return;
|
|
2872
|
+
_inBracketedPaste = true;
|
|
2873
|
+
_bracketedPasteBuffer = '';
|
|
2874
|
+
i = start + PASTE_BEGIN.length;
|
|
2875
|
+
} else {
|
|
2876
|
+
const end = s.indexOf(PASTE_END, i);
|
|
2877
|
+
if (end === -1) {
|
|
2878
|
+
_bracketedPasteBuffer += s.slice(i);
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
_bracketedPasteBuffer += s.slice(i, end);
|
|
2882
|
+
_inBracketedPaste = false;
|
|
2883
|
+
const payload = _bracketedPasteBuffer;
|
|
2884
|
+
_bracketedPasteBuffer = '';
|
|
2885
|
+
// Notify subscribers on next tick so readline finishes emitting its
|
|
2886
|
+
// synchronous `line` events for the buffered content first.
|
|
2887
|
+
const cbs = [..._pasteEndListeners];
|
|
2888
|
+
setImmediate(() => { for (const cb of cbs) { try { cb(payload); } catch {} } });
|
|
2889
|
+
i = end + PASTE_END.length;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// The prompt label is the USER speaking, not the agent. Use the signed-in
|
|
2896
|
+
// GitHub handle if known, otherwise fall back to "You".
|
|
2897
|
+
//
|
|
2898
|
+
// Modern Node readline strips ANSI escapes when calculating prompt width.
|
|
2899
|
+
// Bash-style SOH/STX markers confuse readline redraws on long wrapped input
|
|
2900
|
+
// and can make the first prompt line appear duplicated.
|
|
2901
|
+
function userPrompt() {
|
|
2902
|
+
const who = session.user?.github_username || session.user?.email?.split('@')[0] || 'You';
|
|
2903
|
+
if (term().plain) return `${who} > `;
|
|
2904
|
+
// Brand magenta handle + chevron. No inverse chip, no bold — the color
|
|
2905
|
+
// alone marks this row as user input.
|
|
2906
|
+
return `${paint.brand.primary(who)} ${paint.brand.primary('›')} `;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function printInputBottomRule() {
|
|
2910
|
+
if (isInputDockMounted()) {
|
|
2911
|
+
clearInputPrompt();
|
|
2912
|
+
moveToContent();
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (term().plain) return;
|
|
2916
|
+
process.stderr.write('\n');
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function idleInputTips() {
|
|
2920
|
+
return '[Enter] send [/] commands [Tab] complete [Ctrl+D] details';
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
function executionInputTips() {
|
|
2924
|
+
return 'type any extra context (paths, corrections, follow-ups) · [Enter] send · [Esc] cancel · [Ctrl+P] pause';
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
const rl = readline.createInterface({
|
|
2928
|
+
input: process.stdin,
|
|
2929
|
+
output: process.stderr,
|
|
2930
|
+
prompt: userPrompt(),
|
|
2931
|
+
completer: (line) => {
|
|
2932
|
+
if (line.startsWith('/')) {
|
|
2933
|
+
return [commandCompletions(line), line];
|
|
2934
|
+
}
|
|
2935
|
+
return [[], line];
|
|
2936
|
+
},
|
|
2937
|
+
historySize: 100,
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2940
|
+
// Give approval manager access to readline for pause/resume
|
|
2941
|
+
approval.setReadline(rl);
|
|
2942
|
+
ctx._rl = rl; // expose to /resume command for readline pause
|
|
2943
|
+
let inputActive = false;
|
|
2944
|
+
let slashHintVisible = false;
|
|
2945
|
+
let slashHintRowsVisible = 0;
|
|
2946
|
+
let slashHintItems = [];
|
|
2947
|
+
let slashHintSelected = 0;
|
|
2948
|
+
let slashHintLine = '';
|
|
2949
|
+
|
|
2950
|
+
function promptBottomPaddingLines() {
|
|
2951
|
+
if (!process.stderr.isTTY || term().plain) return 0;
|
|
2952
|
+
// When the dock is mounted the hint borrows the rows below the input
|
|
2953
|
+
// (bottom rule + tips + safety); prepareInputPrompt re-renders the
|
|
2954
|
+
// frame when clearSlashHint() fires, so the rule/tips reappear.
|
|
2955
|
+
// 3 rows fits comfortably in the dock's reservation without leaking
|
|
2956
|
+
// past the safety row.
|
|
2957
|
+
if (isInputDockMounted()) return 3;
|
|
2958
|
+
const raw = process.env.KEPLER_PROMPT_BOTTOM_PADDING ?? '5';
|
|
2959
|
+
const n = Number.parseInt(raw, 10);
|
|
2960
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
2961
|
+
return Math.min(8, n);
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
function truncateHintText(text, max) {
|
|
2965
|
+
const s = String(text || '');
|
|
2966
|
+
if (s.length <= max) return s;
|
|
2967
|
+
if (max <= 1) return '';
|
|
2968
|
+
return s.slice(0, max - 1) + '…';
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function promptColumns() {
|
|
2972
|
+
return stripAnsi(userPrompt()).length;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function restoreReadlineCursor() {
|
|
2976
|
+
const col = Math.max(0, promptColumns() + Number(rl.cursor || 0));
|
|
2977
|
+
readline.cursorTo(process.stderr, col);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
function renderSlashHint(line = '', { preserveSelection = false } = {}) {
|
|
2981
|
+
if (!process.stderr.isTTY || term().plain || !inputActive || !promptBottomPaddingLines()) return;
|
|
2982
|
+
const rows = promptBottomPaddingLines();
|
|
2983
|
+
const suggestions = slashCommandSuggestions(line, Math.min(5, rows));
|
|
2984
|
+
const cols = process.stdout.columns || 80;
|
|
2985
|
+
if (!preserveSelection || line !== slashHintLine) slashHintSelected = 0;
|
|
2986
|
+
slashHintItems = suggestions;
|
|
2987
|
+
slashHintLine = line;
|
|
2988
|
+
if (slashHintSelected >= slashHintItems.length) slashHintSelected = Math.max(0, slashHintItems.length - 1);
|
|
2989
|
+
|
|
2990
|
+
readline.moveCursor(process.stderr, 0, 1);
|
|
2991
|
+
for (let i = 0; i < rows; i++) {
|
|
2992
|
+
readline.clearLine(process.stderr, 0);
|
|
2993
|
+
readline.cursorTo(process.stderr, 0);
|
|
2994
|
+
const item = suggestions[i];
|
|
2995
|
+
if (item) {
|
|
2996
|
+
const marker = i === slashHintSelected ? c.brand('›') : c.dim(' ');
|
|
2997
|
+
const command = item.command.padEnd(13);
|
|
2998
|
+
const maxDesc = Math.max(0, cols - 21);
|
|
2999
|
+
const desc = truncateHintText(item.description, maxDesc);
|
|
3000
|
+
process.stderr.write(` ${marker} ${c.brand(command)}${desc ? c.dim(desc) : ''}`);
|
|
3001
|
+
}
|
|
3002
|
+
if (i < rows - 1) readline.moveCursor(process.stderr, 0, 1);
|
|
3003
|
+
}
|
|
3004
|
+
readline.moveCursor(process.stderr, 0, -rows);
|
|
3005
|
+
restoreReadlineCursor();
|
|
3006
|
+
slashHintVisible = suggestions.length > 0;
|
|
3007
|
+
slashHintRowsVisible = rows;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function clearSlashHint({ restoreCursor: shouldRestoreCursor = true } = {}) {
|
|
3011
|
+
if (!slashHintVisible || !process.stderr.isTTY || term().plain) {
|
|
3012
|
+
slashHintVisible = false;
|
|
3013
|
+
slashHintRowsVisible = 0;
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const rows = slashHintRowsVisible || promptBottomPaddingLines() || 1;
|
|
3017
|
+
readline.moveCursor(process.stderr, 0, 1);
|
|
3018
|
+
for (let i = 0; i < rows; i++) {
|
|
3019
|
+
readline.clearLine(process.stderr, 0);
|
|
3020
|
+
readline.cursorTo(process.stderr, 0);
|
|
3021
|
+
if (i < rows - 1) readline.moveCursor(process.stderr, 0, 1);
|
|
3022
|
+
}
|
|
3023
|
+
readline.moveCursor(process.stderr, 0, -rows);
|
|
3024
|
+
// The dock's bottom rule + tips row live in the rows we just cleared.
|
|
3025
|
+
// Repaint the frame (input row untouched) so they reappear.
|
|
3026
|
+
if (isInputDockMounted()) redrawDockFrame();
|
|
3027
|
+
if (shouldRestoreCursor) restoreReadlineCursor();
|
|
3028
|
+
slashHintVisible = false;
|
|
3029
|
+
slashHintRowsVisible = 0;
|
|
3030
|
+
slashHintItems = [];
|
|
3031
|
+
slashHintSelected = 0;
|
|
3032
|
+
slashHintLine = '';
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
function replaceReadlineLine(value) {
|
|
3036
|
+
const next = String(value || '');
|
|
3037
|
+
rl.line = next;
|
|
3038
|
+
rl.cursor = next.length;
|
|
3039
|
+
if (typeof rl._refreshLine === 'function') {
|
|
3040
|
+
rl._refreshLine();
|
|
3041
|
+
} else {
|
|
3042
|
+
readline.cursorTo(process.stderr, promptColumns());
|
|
3043
|
+
readline.clearLine(process.stderr, 1);
|
|
3044
|
+
process.stderr.write(next);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
function acceptSlashHint() {
|
|
3049
|
+
const item = slashHintItems[slashHintSelected];
|
|
3050
|
+
if (!item) return false;
|
|
3051
|
+
replaceReadlineLine(item.command);
|
|
3052
|
+
slashHintLine = item.command;
|
|
3053
|
+
renderSlashHint(item.command, { preserveSelection: true });
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function moveSlashHintSelection(delta) {
|
|
3058
|
+
if (!slashHintItems.length) return false;
|
|
3059
|
+
const count = slashHintItems.length;
|
|
3060
|
+
slashHintSelected = (slashHintSelected + delta + count) % count;
|
|
3061
|
+
replaceReadlineLine(slashHintLine);
|
|
3062
|
+
renderSlashHint(slashHintLine, { preserveSelection: true });
|
|
3063
|
+
return true;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
function selectedSlashCommandFor(line) {
|
|
3067
|
+
const input = String(line || '').trim();
|
|
3068
|
+
if (!input.startsWith('/')) return null;
|
|
3069
|
+
if (COMMANDS[input] || input.startsWith('/help ')) return input;
|
|
3070
|
+
const item = slashHintItems[slashHintSelected];
|
|
3071
|
+
if (!item) return input;
|
|
3072
|
+
return item.command;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function reservePromptBottomPadding() {
|
|
3076
|
+
const lines = promptBottomPaddingLines();
|
|
3077
|
+
if (!lines) return;
|
|
3078
|
+
process.stderr.write(`${'\n'.repeat(lines)}\x1b[${lines}A\r`);
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function renderIdleDockInput() {
|
|
3082
|
+
if (!isInputDockMounted()) return false;
|
|
3083
|
+
return renderDockInput(userPrompt(), rl.line || '', {
|
|
3084
|
+
context: buildContextStrip(),
|
|
3085
|
+
meta: buildDockMeta(),
|
|
3086
|
+
tips: idleInputTips(),
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
function promptInputLine() {
|
|
3091
|
+
// When the fixed dock is active, readline should own only the input
|
|
3092
|
+
// buffer, not the visual prompt. If readline paints the prompt itself,
|
|
3093
|
+
// long wrapped input can leave a stale duplicate row inside the dock.
|
|
3094
|
+
rl.setPrompt(isInputDockMounted() ? '' : userPrompt());
|
|
3095
|
+
reservePromptBottomPadding();
|
|
3096
|
+
inputActive = true;
|
|
3097
|
+
rl.prompt();
|
|
3098
|
+
renderIdleDockInput();
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
function printSubmittedInput(input) {
|
|
3102
|
+
if (!isInputDockMounted()) {
|
|
3103
|
+
printInputBottomRule();
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const lines = String(input || '').split('\n');
|
|
3107
|
+
printInputBottomRule();
|
|
3108
|
+
process.stderr.write(`${transcriptHeader('you', { tone: 'user' })}\n`);
|
|
3109
|
+
for (const line of lines) {
|
|
3110
|
+
process.stderr.write(`${transcriptLine(line, { tone: 'user' })}\n`);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// Helper: show prompt with separator + vertical breathing room
|
|
3115
|
+
function showPrompt() {
|
|
3116
|
+
if (isInputDockMounted()) {
|
|
3117
|
+
prepareInputPrompt({ context: buildContextStrip(), meta: buildDockMeta(), tips: idleInputTips() });
|
|
3118
|
+
promptInputLine();
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
printPromptBlock();
|
|
3122
|
+
process.stderr.write('\n'); // half-inch vertical gap above input line
|
|
3123
|
+
promptInputLine();
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
showPrompt();
|
|
3127
|
+
|
|
3128
|
+
if (process.stdin.isTTY) {
|
|
3129
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
3130
|
+
process.stdin.on('keypress', (_str, key = {}) => {
|
|
3131
|
+
if (!inputActive) return;
|
|
3132
|
+
setImmediate(() => {
|
|
3133
|
+
if (!inputActive) return;
|
|
3134
|
+
if (slashHintVisible && key.name === 'tab' && acceptSlashHint()) return;
|
|
3135
|
+
if (slashHintVisible && key.name === 'down' && moveSlashHintSelection(1)) return;
|
|
3136
|
+
if (slashHintVisible && key.name === 'up' && moveSlashHintSelection(-1)) return;
|
|
3137
|
+
const isSlash = String(rl.line || '').trimStart().startsWith('/');
|
|
3138
|
+
if (!isSlash) clearSlashHint();
|
|
3139
|
+
// Refresh the dock input FIRST so the frame (bottom rule + tips)
|
|
3140
|
+
// is fresh, THEN paint the slash hint on top — otherwise the
|
|
3141
|
+
// dock repaint would wipe the hint we just wrote.
|
|
3142
|
+
renderIdleDockInput();
|
|
3143
|
+
if (isSlash) renderSlashHint(rl.line);
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// Guard against concurrent line handlers and multiline paste bursts.
|
|
3149
|
+
//
|
|
3150
|
+
// Node readline emits one `line` event per pasted newline. Two mechanisms
|
|
3151
|
+
// coalesce those into a single input:
|
|
3152
|
+
// 1. If the terminal supports bracketed paste, we hold flushing until we
|
|
3153
|
+
// see the ESC[201~ end marker — reliable regardless of paste latency.
|
|
3154
|
+
// 2. Otherwise we fall back to a short timer that merges bursts arriving
|
|
3155
|
+
// within KEPLER_PASTE_FLUSH_MS.
|
|
3156
|
+
let _lineInFlight = false;
|
|
3157
|
+
const _queuedLines = [];
|
|
3158
|
+
let _pasteLines = [];
|
|
3159
|
+
let _pasteFlushTimer = null;
|
|
3160
|
+
|
|
3161
|
+
function pasteFlushDelayMs() {
|
|
3162
|
+
const raw = Number.parseInt(process.env.KEPLER_PASTE_FLUSH_MS || '35', 10);
|
|
3163
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.min(250, raw) : 35;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function queueOrRunLine(line) {
|
|
3167
|
+
if (_lineInFlight) {
|
|
3168
|
+
if (line && line.trim()) _queuedLines.push(line);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
_lineInFlight = true;
|
|
3172
|
+
Promise.resolve()
|
|
3173
|
+
.then(() => _handleLine(line))
|
|
3174
|
+
.finally(() => {
|
|
3175
|
+
_lineInFlight = false;
|
|
3176
|
+
if (_queuedLines.length) {
|
|
3177
|
+
const next = _queuedLines.shift();
|
|
3178
|
+
setImmediate(() => queueOrRunLine(next));
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function flushPastedLines() {
|
|
3184
|
+
if (_pasteFlushTimer) {
|
|
3185
|
+
clearTimeout(_pasteFlushTimer);
|
|
3186
|
+
_pasteFlushTimer = null;
|
|
3187
|
+
}
|
|
3188
|
+
if (!_pasteLines.length) return;
|
|
3189
|
+
const line = _pasteLines.join('\n');
|
|
3190
|
+
_pasteLines = [];
|
|
3191
|
+
queueOrRunLine(line);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
// If we're mid-paste when readline fires `line`, cancel the debounce timer;
|
|
3195
|
+
// the paste-end listener below will flush once the terminal closes the
|
|
3196
|
+
// bracket. If we're NOT in a paste (either the terminal doesn't support it,
|
|
3197
|
+
// or the user pressed Enter normally), the debounce falls back to old
|
|
3198
|
+
// behavior — a single Enter flushes almost instantly.
|
|
3199
|
+
rl.on('line', async (line) => {
|
|
3200
|
+
_pasteLines.push(line);
|
|
3201
|
+
if (_pasteFlushTimer) clearTimeout(_pasteFlushTimer);
|
|
3202
|
+
if (isInBracketedPaste()) {
|
|
3203
|
+
_pasteFlushTimer = null;
|
|
3204
|
+
} else {
|
|
3205
|
+
_pasteFlushTimer = setTimeout(flushPastedLines, pasteFlushDelayMs());
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
|
|
3209
|
+
onBracketedPasteEnd(() => {
|
|
3210
|
+
// Readline has finished emitting synchronous line events for the pasted
|
|
3211
|
+
// content by the time this fires (setImmediate in the pre-listener).
|
|
3212
|
+
flushPastedLines();
|
|
3213
|
+
});
|
|
3214
|
+
|
|
3215
|
+
async function _handleLine(line) {
|
|
3216
|
+
let input = line.trim();
|
|
3217
|
+
const selectedSlashCommand = selectedSlashCommandFor(input);
|
|
3218
|
+
inputActive = false;
|
|
3219
|
+
clearSlashHint();
|
|
3220
|
+
if (selectedSlashCommand) input = selectedSlashCommand;
|
|
3221
|
+
if (!input) {
|
|
3222
|
+
// Empty Enter leaves readline's phantom prompt line ("ravia.sapbpc ›")
|
|
3223
|
+
// committed above the cursor. Wipe it so repeated blank Enters don't
|
|
3224
|
+
// stack into a ladder of empty prompts in the transcript.
|
|
3225
|
+
if (process.stderr.isTTY && !term().plain) {
|
|
3226
|
+
// The paste debounce may have batched N Enters into one flush. The
|
|
3227
|
+
// joined `line` string has one '\n' per additional Enter, so
|
|
3228
|
+
// split('\n').length gives the phantom count.
|
|
3229
|
+
const phantoms = Math.max(1, String(line).split('\n').length);
|
|
3230
|
+
for (let i = 0; i < phantoms; i++) {
|
|
3231
|
+
process.stderr.write('\x1b[A\x1b[2K\r');
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
showPrompt();
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
printSubmittedInput(input);
|
|
3238
|
+
|
|
3239
|
+
// Save to input history
|
|
3240
|
+
session.inputHistory.push(input);
|
|
3241
|
+
|
|
3242
|
+
// Slash commands
|
|
3243
|
+
if (input.startsWith('/')) {
|
|
3244
|
+
await handleCommand(input, ctx);
|
|
3245
|
+
showPrompt();
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// Budget cap (PRD-055 §10). Stop before the next paid call when exceeded.
|
|
3250
|
+
if (session.budgetUsd && session.totalCost >= session.budgetUsd) {
|
|
3251
|
+
session.budgetExceeded = true;
|
|
3252
|
+
process.stderr.write(` ${c.yellow('⏹')} ${c.dim(`Budget reached ($${session.totalCost.toFixed(2)} of $${session.budgetUsd.toFixed(2)}). Use /budget clear to continue.`)}\n`);
|
|
3253
|
+
showPrompt();
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
const originalInput = input;
|
|
3258
|
+
const creds = auth.loadCredentials();
|
|
3259
|
+
if (!creds.token) {
|
|
3260
|
+
process.stderr.write(` ${c.red('Not logged in. Run /login first.')}\n`);
|
|
3261
|
+
showPrompt();
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// Create or reuse stream client — sessionId persists across turns.
|
|
3266
|
+
// The same client also owns the authenticated vision-analysis preflight.
|
|
3267
|
+
if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token) {
|
|
3268
|
+
streamClient = new TarangStreamClient({
|
|
3269
|
+
baseUrl: creds.backendUrl,
|
|
3270
|
+
token: creds.token,
|
|
3271
|
+
toolExecutor,
|
|
3272
|
+
approvalManager: approval,
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
const client = streamClient;
|
|
3276
|
+
if (session.id && !client.sessionId) {
|
|
3277
|
+
client.sessionId = session.id;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
try {
|
|
3281
|
+
const pending = pendingVisionPaths(ctx);
|
|
3282
|
+
const prepared = prepareImageAttachments(originalInput, {
|
|
3283
|
+
cwd: safeCwd(),
|
|
3284
|
+
extraPaths: pending,
|
|
3285
|
+
});
|
|
3286
|
+
if (prepared.attachments.length) {
|
|
3287
|
+
process.stderr.write(` ${c.brand('◇')} ${c.dim(`attached ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}:`)} ${prepared.attachments.map(attachmentSummaryLine).join(c.dim(' · '))}\n`);
|
|
3288
|
+
jsonlWriter.writeKeplerEvent({
|
|
3289
|
+
type: 'attachments',
|
|
3290
|
+
data: { attachments: prepared.attachments.map(publicAttachmentMetadata) },
|
|
3291
|
+
});
|
|
3292
|
+
const approved = await confirmVisionUpload(ctx, prepared.attachments, { skip: skipPerms });
|
|
3293
|
+
pending.length = 0;
|
|
3294
|
+
if (!approved) {
|
|
3295
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('Vision upload skipped; continuing without image analysis.')}\n`);
|
|
3296
|
+
input = prepared.instruction || originalInput;
|
|
3297
|
+
} else {
|
|
3298
|
+
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Analyzing image...')}\r`);
|
|
3299
|
+
const analysis = await client.analyzeVision({
|
|
3300
|
+
instruction: prepared.instruction,
|
|
3301
|
+
attachments: prepared.attachments,
|
|
3302
|
+
});
|
|
3303
|
+
process.stderr.write(`\r${' '.repeat(80)}\r`);
|
|
3304
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Vision analysis completed for ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}`)}${analysis.model ? c.dim(` · ${analysis.model}`) : ''}\n`);
|
|
3305
|
+
jsonlWriter.writeKeplerEvent({
|
|
3306
|
+
type: 'vision_analysis',
|
|
3307
|
+
data: {
|
|
3308
|
+
model: analysis.model || '',
|
|
3309
|
+
summary_chars: String(analysis.summary || '').length,
|
|
3310
|
+
attachments: analysis.attachments || prepared.metadata,
|
|
3311
|
+
},
|
|
3312
|
+
});
|
|
3313
|
+
input = appendVisionAnalysisToInstruction(prepared.instruction, analysis);
|
|
3314
|
+
}
|
|
3315
|
+
} else {
|
|
3316
|
+
input = prepared.instruction || originalInput;
|
|
3317
|
+
}
|
|
3318
|
+
} catch (err) {
|
|
3319
|
+
process.stderr.write(` ${c.red('Vision error: ' + (err.message || String(err)))}\n`);
|
|
3320
|
+
showPrompt();
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// Regular prompt
|
|
3325
|
+
const userMessage = { role: 'user', content: input };
|
|
3326
|
+
session.history.push(userMessage);
|
|
3327
|
+
session.agentHistory.push(userMessage);
|
|
3328
|
+
session.turns++;
|
|
3329
|
+
session.toolCalls = 0;
|
|
3330
|
+
session.subAgentToolCalls = 0;
|
|
3331
|
+
session.lastTask = originalInput;
|
|
3332
|
+
// Reset per-turn counts so the mission report reflects this turn only.
|
|
3333
|
+
session.toolCounts = {};
|
|
3334
|
+
session.subAgentCounts = {};
|
|
3335
|
+
session.filesRead = [];
|
|
3336
|
+
session.savedUsd = 0;
|
|
3337
|
+
session._lastEmittedThinking = '';
|
|
3338
|
+
session.creditsLowWarned = false;
|
|
3339
|
+
session.msgsLowWarned = false;
|
|
3340
|
+
|
|
3341
|
+
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
3342
|
+
// task / turn counters in the status bar.
|
|
3343
|
+
if (orbitRef.current) orbitRef.current.onUserInput(originalInput);
|
|
3344
|
+
|
|
3345
|
+
// Start session tracking on first turn
|
|
3346
|
+
if (session.turns === 1) {
|
|
3347
|
+
sessionMgr.start(originalInput);
|
|
3348
|
+
}
|
|
3349
|
+
let userTurnWritten = false;
|
|
3350
|
+
const writeCurrentUserTurn = () => {
|
|
3351
|
+
if (userTurnWritten) return;
|
|
3352
|
+
jsonlWriter.writeUserTurn(input);
|
|
3353
|
+
jsonlWriter.writeHistory(input);
|
|
3354
|
+
userTurnWritten = true;
|
|
3355
|
+
};
|
|
3356
|
+
if (session.id) writeCurrentUserTurn();
|
|
3357
|
+
|
|
3358
|
+
let assistantContent = '';
|
|
3359
|
+
const agentTurnHistory = new AgentHistoryTurnBuilder();
|
|
3360
|
+
|
|
3361
|
+
// ── Execution keypress listener (Esc = cancel, Space = pause/resume) ──
|
|
3362
|
+
let executionPaused = false;
|
|
3363
|
+
let keypressCleanup = null;
|
|
3364
|
+
let execListenerActive = false;
|
|
3365
|
+
let lastCtrlCAt = 0; // PRD-055 §8.4: first Ctrl+C cancels, second exits
|
|
3366
|
+
let executionInputBuffer = '';
|
|
3367
|
+
let executionInputVisible = false;
|
|
3368
|
+
|
|
3369
|
+
function executionInputPrefix() {
|
|
3370
|
+
// Inviting prompt: brand '+' + hint that this accepts any extra
|
|
3371
|
+
// context (paths, corrections, more instructions). Visible even when
|
|
3372
|
+
// the buffer is empty so users know they can type mid-run.
|
|
3373
|
+
return `${paint.brand.data('+')} ${paint.dim('add instruction')} ${paint.dim('›')} `;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
function redrawExecutionInput() {
|
|
3377
|
+
if (isInputDockMounted()) {
|
|
3378
|
+
renderDockInput(executionInputPrefix(), executionInputBuffer, {
|
|
3379
|
+
context: buildContextStrip(),
|
|
3380
|
+
meta: buildDockMeta(),
|
|
3381
|
+
tips: executionInputTips(),
|
|
3382
|
+
});
|
|
3383
|
+
executionInputVisible = true;
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
if (!executionInputVisible) {
|
|
3387
|
+
stopSpinner();
|
|
3388
|
+
process.stderr.write(`\n${executionInputPrefix()}`);
|
|
3389
|
+
executionInputVisible = true;
|
|
3390
|
+
}
|
|
3391
|
+
readline.clearLine(process.stderr, 0);
|
|
3392
|
+
readline.cursorTo(process.stderr, 0);
|
|
3393
|
+
process.stderr.write(`${executionInputPrefix()}${executionInputBuffer}`);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function focusExecutionInput() {
|
|
3397
|
+
if (!isInputDockMounted()) return;
|
|
3398
|
+
focusDockInput(executionInputPrefix(), executionInputBuffer);
|
|
3399
|
+
}
|
|
3400
|
+
runtime.afterContentFlush = focusExecutionInput;
|
|
3401
|
+
|
|
3402
|
+
async function submitExecutionInstruction() {
|
|
3403
|
+
const instruction = executionInputBuffer.trim();
|
|
3404
|
+
executionInputBuffer = '';
|
|
3405
|
+
if (!instruction) {
|
|
3406
|
+
if (isInputDockMounted()) {
|
|
3407
|
+
clearInputPrompt();
|
|
3408
|
+
renderDockInput(executionInputPrefix(), '', {
|
|
3409
|
+
context: buildContextStrip(),
|
|
3410
|
+
meta: buildDockMeta(),
|
|
3411
|
+
tips: executionInputTips(),
|
|
3412
|
+
});
|
|
3413
|
+
moveToContent();
|
|
3414
|
+
} else if (executionInputVisible) {
|
|
3415
|
+
process.stderr.write('\n');
|
|
3416
|
+
}
|
|
3417
|
+
executionInputVisible = false;
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
if (isInputDockMounted()) {
|
|
3421
|
+
clearInputPrompt();
|
|
3422
|
+
moveToContent();
|
|
3423
|
+
process.stderr.write(`${executionInputPrefix()}${instruction}\n`);
|
|
3424
|
+
renderDockInput(executionInputPrefix(), '', {
|
|
3425
|
+
context: buildContextStrip(),
|
|
3426
|
+
meta: buildDockMeta(),
|
|
3427
|
+
tips: executionInputTips(),
|
|
3428
|
+
});
|
|
3429
|
+
moveToContent();
|
|
3430
|
+
} else if (executionInputVisible) {
|
|
3431
|
+
process.stderr.write('\n');
|
|
3432
|
+
}
|
|
3433
|
+
executionInputVisible = false;
|
|
3434
|
+
try {
|
|
3435
|
+
await client.resume(instruction);
|
|
3436
|
+
jsonlWriter.writeKeplerEvent({
|
|
3437
|
+
type: 'user_intervention',
|
|
3438
|
+
data: { instruction, task_id: client.currentTaskId || null },
|
|
3439
|
+
});
|
|
3440
|
+
process.stderr.write(` ${c.green('↳')} ${c.dim('sent follow-up to running agent')}\n`);
|
|
3441
|
+
} catch {
|
|
3442
|
+
_queuedLines.push(instruction);
|
|
3443
|
+
process.stderr.write(` ${c.yellow('↳')} ${c.dim('queued follow-up for the next turn')}\n`);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
function appendExecutionInput(text) {
|
|
3448
|
+
if (!text) return;
|
|
3449
|
+
executionInputBuffer += text;
|
|
3450
|
+
redrawExecutionInput();
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
function backspaceExecutionInput() {
|
|
3454
|
+
if (!executionInputBuffer) return false;
|
|
3455
|
+
executionInputBuffer = executionInputBuffer.slice(0, -1);
|
|
3456
|
+
redrawExecutionInput();
|
|
3457
|
+
return true;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
if (process.stdin.isTTY) {
|
|
3461
|
+
rl.pause();
|
|
3462
|
+
const wasRaw = process.stdin.isRaw;
|
|
3463
|
+
process.stdin.setRawMode(true);
|
|
3464
|
+
process.stdin.resume();
|
|
3465
|
+
execListenerActive = true;
|
|
3466
|
+
|
|
3467
|
+
// Bracketed-paste state for follow-up input during execution.
|
|
3468
|
+
let execPasteActive = false;
|
|
3469
|
+
let execPasteBuffer = '';
|
|
3470
|
+
|
|
3471
|
+
// Accept any character that isn't a bare C0 control (except tab) and
|
|
3472
|
+
// isn't part of an ESC sequence. Unicode, emoji, and tabs all pass.
|
|
3473
|
+
const isSafeFollowUpText = (s) => {
|
|
3474
|
+
if (!s) return false;
|
|
3475
|
+
if (s.includes('\x1b')) return false; // escape / arrow / meta keys
|
|
3476
|
+
for (const ch of s) {
|
|
3477
|
+
const code = ch.codePointAt(0);
|
|
3478
|
+
if (code === 0x09) continue; // tab ok
|
|
3479
|
+
if (code < 0x20 || code === 0x7f) return false;
|
|
3480
|
+
}
|
|
3481
|
+
return true;
|
|
3482
|
+
};
|
|
3483
|
+
|
|
3484
|
+
const onData = (data) => {
|
|
3485
|
+
if (!execListenerActive) return; // paused for approval menu
|
|
3486
|
+
const bytes = [...data];
|
|
3487
|
+
const text = data.toString('utf8');
|
|
3488
|
+
|
|
3489
|
+
// ── Bracketed paste passthrough ────────────────────────────────────
|
|
3490
|
+
// Strip ESC[200~/ESC[201~ markers and treat the content between them
|
|
3491
|
+
// as a single append. Handles pastes that straddle chunk boundaries.
|
|
3492
|
+
if (execPasteActive || text.includes(PASTE_BEGIN)) {
|
|
3493
|
+
let s = text;
|
|
3494
|
+
while (s.length) {
|
|
3495
|
+
if (!execPasteActive) {
|
|
3496
|
+
const start = s.indexOf(PASTE_BEGIN);
|
|
3497
|
+
if (start === -1) break;
|
|
3498
|
+
// Anything before the start marker is normal keystrokes;
|
|
3499
|
+
// let it fall through by re-invoking with just that slice.
|
|
3500
|
+
if (start > 0) {
|
|
3501
|
+
const pre = s.slice(0, start);
|
|
3502
|
+
// Recurse via a synthetic buffer so normal handlers process
|
|
3503
|
+
// the pre-paste characters below.
|
|
3504
|
+
onData(Buffer.from(pre, 'utf8'));
|
|
3505
|
+
}
|
|
3506
|
+
execPasteActive = true;
|
|
3507
|
+
execPasteBuffer = '';
|
|
3508
|
+
s = s.slice(start + PASTE_BEGIN.length);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
const end = s.indexOf(PASTE_END);
|
|
3512
|
+
if (end === -1) { execPasteBuffer += s; return; }
|
|
3513
|
+
execPasteBuffer += s.slice(0, end);
|
|
3514
|
+
execPasteActive = false;
|
|
3515
|
+
const payload = execPasteBuffer;
|
|
3516
|
+
execPasteBuffer = '';
|
|
3517
|
+
if (payload) appendExecutionInput(payload);
|
|
3518
|
+
s = s.slice(end + PASTE_END.length);
|
|
3519
|
+
}
|
|
3520
|
+
if (!s.length) return;
|
|
3521
|
+
// Anything after the end marker (rare) falls through as a fresh
|
|
3522
|
+
// buffer for the normal handlers.
|
|
3523
|
+
data = Buffer.from(s, 'utf8');
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
const bytes2 = [...data];
|
|
3527
|
+
const text2 = data.toString('utf8');
|
|
3528
|
+
|
|
3529
|
+
// Esc key (single byte 0x1b, not part of arrow sequence)
|
|
3530
|
+
if (bytes2.length === 1 && bytes2[0] === 0x1b) {
|
|
3531
|
+
if (executionInputVisible || executionInputBuffer) {
|
|
3532
|
+
executionInputBuffer = '';
|
|
3533
|
+
if (isInputDockMounted()) {
|
|
3534
|
+
clearInputPrompt();
|
|
3535
|
+
renderDockInput(executionInputPrefix(), '', {
|
|
3536
|
+
context: buildContextStrip(),
|
|
3537
|
+
meta: buildDockMeta(),
|
|
3538
|
+
tips: executionInputTips(),
|
|
3539
|
+
});
|
|
3540
|
+
moveToContent();
|
|
3541
|
+
} else if (executionInputVisible) {
|
|
3542
|
+
readline.clearLine(process.stderr, 0);
|
|
3543
|
+
readline.cursorTo(process.stderr, 0);
|
|
3544
|
+
}
|
|
3545
|
+
executionInputVisible = false;
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
stopSpinner();
|
|
3549
|
+
if (isInputDockMounted()) {
|
|
3550
|
+
clearInputPrompt();
|
|
3551
|
+
moveToContent();
|
|
3552
|
+
}
|
|
3553
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled.')}\n`);
|
|
3554
|
+
// cancel() now aborts the in-flight SSE reader; the for-await loop
|
|
3555
|
+
// wakes up immediately and the prompt returns. No more "stuck"
|
|
3556
|
+
// Cancelling… message.
|
|
3557
|
+
client.cancel();
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
if (bytes2.length === 1 && (bytes2[0] === 0x7f || bytes2[0] === 0x08)) {
|
|
3562
|
+
backspaceExecutionInput();
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
if (text2.includes('\r') || text2.includes('\n')) {
|
|
3567
|
+
const parts = text2.split(/\r?\n|\r/);
|
|
3568
|
+
if (parts[0]) appendExecutionInput(parts[0]);
|
|
3569
|
+
submitExecutionInstruction();
|
|
3570
|
+
// Extra lines from a raw (non-bracketed) paste get concatenated
|
|
3571
|
+
// into the current follow-up buffer instead of firing multiple
|
|
3572
|
+
// resume() calls. If the user presses Enter again, they send.
|
|
3573
|
+
for (const extra of parts.slice(1)) {
|
|
3574
|
+
if (extra) appendExecutionInput('\n' + extra);
|
|
3575
|
+
}
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// Ctrl+P — pause/resume (moved off Space so follow-up input can start
|
|
3580
|
+
// with a space without triggering pause).
|
|
3581
|
+
if (bytes2.length === 1 && bytes2[0] === 0x10) {
|
|
3582
|
+
if (executionPaused) {
|
|
3583
|
+
executionPaused = false;
|
|
3584
|
+
if (isInputDockMounted()) moveToContent();
|
|
3585
|
+
process.stderr.write(` ${c.green('▶')} ${c.dim('Resumed')}\n`);
|
|
3586
|
+
client.resume();
|
|
3587
|
+
if (orbitRef.current) orbitRef.current.onResume();
|
|
3588
|
+
} else {
|
|
3589
|
+
executionPaused = true;
|
|
3590
|
+
stopSpinner();
|
|
3591
|
+
if (isInputDockMounted()) moveToContent();
|
|
3592
|
+
process.stderr.write(` ${c.yellow('⏸')} ${c.dim('Paused — press Ctrl+P to resume, Esc to cancel')}\n`);
|
|
3593
|
+
client.pause();
|
|
3594
|
+
if (orbitRef.current) orbitRef.current.onPause();
|
|
3595
|
+
}
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// Ctrl+C during execution — PRD-055 §8.4 two-step semantics:
|
|
3600
|
+
// first press → cancel current backend run, stay in REPL
|
|
3601
|
+
// second press within 2s → exit the CLI
|
|
3602
|
+
if (bytes2[0] === 0x03) {
|
|
3603
|
+
stopSpinner();
|
|
3604
|
+
const now = Date.now();
|
|
3605
|
+
if (lastCtrlCAt && (now - lastCtrlCAt) < 2000) {
|
|
3606
|
+
if (isInputDockMounted()) unmountInputDock();
|
|
3607
|
+
process.stderr.write(`\n ${c.dim('exiting…')}\n`);
|
|
3608
|
+
try { client.cancel(); } catch {}
|
|
3609
|
+
process.exit(0);
|
|
3610
|
+
}
|
|
3611
|
+
lastCtrlCAt = now;
|
|
3612
|
+
if (isInputDockMounted()) {
|
|
3613
|
+
clearInputPrompt();
|
|
3614
|
+
moveToContent();
|
|
3615
|
+
}
|
|
3616
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled. Press Ctrl+C again within 2s to exit.')}\n`);
|
|
3617
|
+
try { client.cancel(); } catch {}
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// Ctrl+D — expand last tool card (Mission Control §6.2). Only when
|
|
3622
|
+
// there's no in-progress follow-up input.
|
|
3623
|
+
if (!executionInputBuffer && bytes2.length === 1 && bytes2[0] === 0x04) {
|
|
3624
|
+
stopSpinner();
|
|
3625
|
+
if (isInputDockMounted()) moveToContent();
|
|
3626
|
+
expandLast();
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// Any safe text (unicode, tabs, spaces, symbols) becomes a live
|
|
3631
|
+
// follow-up instruction. Enter sends it via resume(instruction).
|
|
3632
|
+
if (isSafeFollowUpText(text2)) {
|
|
3633
|
+
appendExecutionInput(text2);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
};
|
|
3637
|
+
|
|
3638
|
+
process.stdin.on('data', onData);
|
|
3639
|
+
|
|
3640
|
+
// Let approval manager pause/resume this listener
|
|
3641
|
+
approval.setExecutionHooks({
|
|
3642
|
+
onPause: () => { execListenerActive = false; },
|
|
3643
|
+
onResume: () => { execListenerActive = true; },
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3646
|
+
keypressCleanup = () => {
|
|
3647
|
+
process.stdin.removeListener('data', onData);
|
|
3648
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
3649
|
+
execListenerActive = false;
|
|
3650
|
+
approval.setExecutionHooks({}); // clear hooks
|
|
3651
|
+
rl.resume();
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
try {
|
|
3656
|
+
if (isInputDockMounted()) {
|
|
3657
|
+
renderDockInput(executionInputPrefix(), '', {
|
|
3658
|
+
context: buildContextStrip(),
|
|
3659
|
+
meta: buildDockMeta(),
|
|
3660
|
+
tips: executionInputTips(),
|
|
3661
|
+
});
|
|
3662
|
+
moveToContent();
|
|
3663
|
+
}
|
|
3664
|
+
startContentStream();
|
|
3665
|
+
process.stderr.write(`\n${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
3666
|
+
runtime.contentHeaderPrinted = true;
|
|
3667
|
+
|
|
3668
|
+
// Immediate feedback so the screen isn't blank between submit and the
|
|
3669
|
+
// first backend event. The first `status`, `thinking`, or `content_*`
|
|
3670
|
+
// event will replace this text; stopSpinner clears it before content
|
|
3671
|
+
// renders.
|
|
3672
|
+
startSpinner('thinking…');
|
|
3673
|
+
|
|
3674
|
+
const execContext = { cwd: safeCwd() };
|
|
3675
|
+
if (skipPerms) execContext.freeswim = true;
|
|
3676
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
3677
|
+
approval.policy = effectivePolicy.policy;
|
|
3678
|
+
if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
|
|
3679
|
+
hookRunner.reload();
|
|
3680
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
|
|
3681
|
+
let projectResources = toolExecutor.getProjectResources();
|
|
3682
|
+
const promptRoots = promptProjectRoots(input);
|
|
3683
|
+
if (promptRoots.length > 0) {
|
|
3684
|
+
await toolExecutor.registerProjectRoots(promptRoots);
|
|
3685
|
+
projectResources = toolExecutor.getProjectResources();
|
|
3686
|
+
}
|
|
3687
|
+
const promptHook = await hookRunner.run('UserPromptSubmit', {
|
|
3688
|
+
input: { prompt: input },
|
|
3689
|
+
turnId: String(session.turns),
|
|
3690
|
+
});
|
|
3691
|
+
const hookHints = (promptHook.results || [])
|
|
3692
|
+
.map(r => r.parsed?.feedback)
|
|
3693
|
+
.filter(Boolean)
|
|
3694
|
+
.map(text => ({ source: 'hook', kind: 'feedback', text, ttl_turns: 1, priority: 'medium' }));
|
|
3695
|
+
const rejectionHints = (approval.consumeRejectionHints?.() || [])
|
|
3696
|
+
.map(h => ({
|
|
3697
|
+
source: 'hitl',
|
|
3698
|
+
kind: 'approval_rejection',
|
|
3699
|
+
text: `${h.decision === 'replan' ? 'User requested a re-plan' : 'User rejected approval'} for ${h.tool}. ${h.note ? `Reason: ${h.note}` : h.reason}. Adjust the approach before retrying.`,
|
|
3700
|
+
ttl_turns: 1,
|
|
3701
|
+
priority: 'high',
|
|
3702
|
+
}));
|
|
3703
|
+
latestEnvelope = buildContextEnvelope({
|
|
3704
|
+
cwd: safeCwd(),
|
|
3705
|
+
effectivePolicy,
|
|
3706
|
+
projectContext: latestProjectContext,
|
|
3707
|
+
activeHints: [...hookHints, ...rejectionHints],
|
|
3708
|
+
projectResources,
|
|
3709
|
+
agentContext: toolExecutor.getAgentContext(),
|
|
3710
|
+
});
|
|
3711
|
+
ctx.effectivePolicy = effectivePolicy;
|
|
3712
|
+
ctx.latestProjectContext = latestProjectContext;
|
|
3713
|
+
ctx.latestEnvelope = latestEnvelope;
|
|
3714
|
+
Object.assign(execContext, latestEnvelope);
|
|
3715
|
+
if (skipPerms) execContext.freeswim = true;
|
|
3716
|
+
const modelOverrides = Object.fromEntries(sessionModelOverrideEntries());
|
|
3717
|
+
if (Object.keys(modelOverrides).length > 0) {
|
|
3718
|
+
execContext.model_overrides = modelOverrides;
|
|
3719
|
+
if (modelOverrides.reasoning) execContext.model_override = modelOverrides.reasoning;
|
|
3720
|
+
}
|
|
3721
|
+
// PRD-071: seed work_scope from CLI so the backend has a byte-stable
|
|
3722
|
+
// scope block from turn 1. Uses projectResources already gathered by
|
|
3723
|
+
// the envelope above.
|
|
3724
|
+
execContext.work_scope = buildWorkScope({
|
|
3725
|
+
instruction: input,
|
|
3726
|
+
cwd: safeCwd(),
|
|
3727
|
+
projectResources,
|
|
3728
|
+
});
|
|
3729
|
+
for (const file of latestProjectContext.changed || []) {
|
|
3730
|
+
if (effectivePolicy.policy.context?.showReloadNotice) {
|
|
3731
|
+
process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
for await (const event of client.execute(input, execContext, session.agentHistory)) {
|
|
3736
|
+
jsonlWriter.writeKeplerEvent(event);
|
|
3737
|
+
if (event.type === 'plan_created' || event.type === 'goal_created') {
|
|
3738
|
+
persistProjectArtifacts(
|
|
3739
|
+
event.data,
|
|
3740
|
+
toolExecutor.getProjectResources(),
|
|
3741
|
+
message => process.stderr.write(` ${c.dim(message)}\n`),
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
if (isInputDockMounted()) moveToContent();
|
|
3745
|
+
renderEvent(event);
|
|
3746
|
+
focusExecutionInput();
|
|
3747
|
+
|
|
3748
|
+
if (event.type === 'content_partial') {
|
|
3749
|
+
const text = event.data?.text || '';
|
|
3750
|
+
assistantContent += text;
|
|
3751
|
+
agentTurnHistory.addAssistantText(text);
|
|
3752
|
+
jsonlWriter.accumulateContent(text);
|
|
3753
|
+
} else if (event.type === 'content') {
|
|
3754
|
+
const text = event.data?.text || '';
|
|
3755
|
+
const newText = assistantContent && text.startsWith(assistantContent)
|
|
3756
|
+
? text.slice(assistantContent.length)
|
|
3757
|
+
: text === assistantContent ? '' : text;
|
|
3758
|
+
if (text) {
|
|
3759
|
+
assistantContent = assistantContent && !text.startsWith(assistantContent)
|
|
3760
|
+
? assistantContent + text
|
|
3761
|
+
: text;
|
|
3762
|
+
}
|
|
3763
|
+
if (newText) {
|
|
3764
|
+
agentTurnHistory.addAssistantText(newText);
|
|
3765
|
+
jsonlWriter.accumulateContent(newText);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// Local JSONL: capture session ID from backend
|
|
3770
|
+
if (event.type === 'session_info' && event.data?.session_id) {
|
|
3771
|
+
jsonlWriter.setSessionId(event.data.session_id);
|
|
3772
|
+
hookRunner.sessionId = event.data.session_id;
|
|
3773
|
+
writeCurrentUserTurn();
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
// Local JSONL: accumulate tool calls
|
|
3777
|
+
if (event.type === 'tool_call' || event.type === 'tool_request') {
|
|
3778
|
+
const d = event.data || {};
|
|
3779
|
+
agentTurnHistory.addToolUse(d);
|
|
3780
|
+
jsonlWriter.accumulateToolCall(d.call_id || d.request_id, d.tool, d.args);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
// Local JSONL: record tool results
|
|
3784
|
+
if (event.type === 'tool_done' || event.type === 'tool_result') {
|
|
3785
|
+
const d = event.data || {};
|
|
3786
|
+
agentTurnHistory.addToolResult(d);
|
|
3787
|
+
jsonlWriter.recordToolResult(d.call_id || d._callId, d.output, d.success === false, d);
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// Local JSONL: flush assistant turn on complete
|
|
3791
|
+
if (event.type === 'complete') {
|
|
3792
|
+
jsonlWriter.setTurnUsage(event.data?.usage, session.model);
|
|
3793
|
+
jsonlWriter.flushAssistantTurn();
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
flushContent();
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
inPlace('');
|
|
3800
|
+
flushContent();
|
|
3801
|
+
process.stderr.write(` ${c.red('Error: ' + err.message)}\n`);
|
|
3802
|
+
} finally {
|
|
3803
|
+
// Clean up execution keypress listener
|
|
3804
|
+
runtime.afterContentFlush = null;
|
|
3805
|
+
if (keypressCleanup) keypressCleanup();
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
if (assistantContent) {
|
|
3809
|
+
const assistantMessage = { role: 'assistant', content: assistantContent };
|
|
3810
|
+
session.history.push(assistantMessage);
|
|
3811
|
+
}
|
|
3812
|
+
const structuredTurn = agentTurnHistory.finish();
|
|
3813
|
+
if (structuredTurn.length) {
|
|
3814
|
+
session.agentHistory.push(...structuredTurn);
|
|
3815
|
+
} else if (assistantContent) {
|
|
3816
|
+
session.agentHistory.push({ role: 'assistant', content: assistantContent });
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
showPrompt();
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
rl.on('close', async () => {
|
|
3823
|
+
clearSlashHint({ restoreCursor: false });
|
|
3824
|
+
inputActive = false;
|
|
3825
|
+
stopSpinner();
|
|
3826
|
+
if (isInputDockMounted()) unmountInputDock();
|
|
3827
|
+
await hookRunner.run('Stop', { input: { session_id: session.id || '' } });
|
|
3828
|
+
await jsonlWriter.close();
|
|
3829
|
+
process.stderr.write(`\n ${c.dim('session ended')}\n\n`);
|
|
3830
|
+
process.exit(0);
|
|
3831
|
+
});
|
|
3832
|
+
}
|