@bahulam/code 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +39 -0
- package/package.json +8 -9
- package/pulse/lib/tool-categories.ts +13 -0
- package/src/commands/device.mjs +121 -0
- package/src/commands/pair.mjs +190 -0
- package/src/commands/remote.mjs +110 -0
- package/src/config/env.mjs +2 -2
- package/src/core/event-log.mjs +393 -0
- package/src/core/headless.mjs +198 -0
- package/src/core/loop.mjs +276 -0
- package/src/core/memory-disk.mjs +210 -0
- package/src/core/paths.mjs +36 -0
- package/src/core/stream-client.mjs +28 -9
- package/src/core/tool-executor.mjs +64 -16
- package/src/daemon/approval-store.mjs +253 -0
- package/src/daemon/attach-client.mjs +361 -0
- package/src/daemon/daemonize.mjs +151 -0
- package/src/daemon/event-tap.mjs +197 -0
- package/src/daemon/input-lock.mjs +191 -0
- package/src/daemon/relay-client.mjs +258 -0
- package/src/daemon/session-core.mjs +179 -0
- package/src/daemon/session-list.mjs +26 -0
- package/src/daemon/session-publisher.mjs +78 -0
- package/src/daemon/socket-server.mjs +329 -0
- package/src/daemon/stop-daemon.mjs +18 -0
- package/src/permissions/checker.mjs +6 -6
- package/src/permissions/prompt.mjs +8 -7
- package/src/skills/installer.mjs +8 -0
- package/src/terminal/ansi.mjs +85 -9
- package/src/terminal/main.mjs +97 -3
- package/src/terminal/repl.mjs +389 -6
- package/src/terminal/skills-picker.mjs +121 -0
- package/src/terminal/skills.mjs +3 -3
- package/src/tools/analyze-code.mjs +39 -0
- package/src/tools/bash.mjs +1 -1
- package/src/tools/edit.mjs +18 -18
- package/src/tools/git-diff.mjs +34 -0
- package/src/tools/git-status.mjs +30 -0
- package/src/tools/glob.mjs +5 -2
- package/src/tools/grep.mjs +1 -1
- package/src/tools/meta-tools.mjs +85 -0
- package/src/tools/read-files.mjs +37 -0
- package/src/tools/read.mjs +20 -10
- package/src/tools/registry.mjs +20 -0
- package/src/tools/remember.mjs +147 -0
- package/src/tools/search-files.mjs +41 -0
- package/src/tools/write-project.mjs +62 -0
- package/src/tools/write.mjs +1 -1
- package/src/ui/banner.mjs +1 -1
- package/src/ui/slash-commands.mjs +16 -0
- package/src/ui/sub-agent.mjs +8 -2
- package/src/ui/transcript-block.mjs +4 -1
package/src/terminal/repl.mjs
CHANGED
|
@@ -25,7 +25,18 @@ import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCred
|
|
|
25
25
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
26
26
|
import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
|
|
27
27
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
28
|
+
import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
|
|
29
|
+
import { startSocketServer } from '../daemon/socket-server.mjs';
|
|
30
|
+
import { resolvePending, interceptApproval, shutdownAllPending, setTimeoutPolicy } from '../daemon/approval-store.mjs';
|
|
31
|
+
import { wireEmit as wireInputLockEmit, resetInputLock } from '../daemon/input-lock.mjs';
|
|
32
|
+
import { startRelayBridge } from '../daemon/relay-client.mjs';
|
|
33
|
+
import { loadRemoteConfig } from '../commands/remote.mjs';
|
|
28
34
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
35
|
+
// PRD-091 Phase 3 preview: opt-in gateway loop path. Set
|
|
36
|
+
// BAHULAM_USE_GATEWAY_LOOP=1 to route the REPL's turn through
|
|
37
|
+
// /v1/agent/turn instead of the local bundled runtime. Falls back to
|
|
38
|
+
// the existing local-agent path if the flag is unset.
|
|
39
|
+
import { createAgentLoop, createGatewaySession } from '../core/loop.mjs';
|
|
29
40
|
import { buildWorkScope, promptProjectRoots } from '../core/work-scope.mjs';
|
|
30
41
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
31
42
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
@@ -49,6 +60,9 @@ import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
|
49
60
|
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
50
61
|
import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
|
|
51
62
|
import { BUILTIN_AGENTS, findBuiltinAgent, localAgentMatches, runAgent, runAgentDefinition } from './agents.mjs';
|
|
63
|
+
import { SkillInstaller } from '../skills/installer.mjs';
|
|
64
|
+
import { SkillsLoader } from '../skills/loader.mjs';
|
|
65
|
+
import { openSkillsPicker, formatSkillsList } from './skills-picker.mjs';
|
|
52
66
|
import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
53
67
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
54
68
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
@@ -939,6 +953,163 @@ async function handleAgentsCommand(rest = '', ctx) {
|
|
|
939
953
|
printAgentsUsage();
|
|
940
954
|
}
|
|
941
955
|
|
|
956
|
+
function printSkillsUsage() {
|
|
957
|
+
process.stderr.write(` ${c.dim('Usage:')}\n`);
|
|
958
|
+
process.stderr.write(` ${c.dim(' /skills open interactive picker')}\n`);
|
|
959
|
+
process.stderr.write(` ${c.dim(' /skills list [--project] list installed skills')}\n`);
|
|
960
|
+
process.stderr.write(` ${c.dim(' /skills install <url|path> [--project] [--force]')}\n`);
|
|
961
|
+
process.stderr.write(` ${c.dim(' /skills view <name> [resource]')}\n`);
|
|
962
|
+
process.stderr.write(` ${c.dim(' /skills remove <name> [--project]')}\n`);
|
|
963
|
+
process.stderr.write(` ${c.dim(' /skills update <name> [--project]')}\n`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function handleSkillsCommand(rest = '', ctx) {
|
|
967
|
+
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
968
|
+
const hasFlag = (flag) => parts.includes(flag);
|
|
969
|
+
const positional = parts.filter(p => !p.startsWith('--'));
|
|
970
|
+
const action = (positional[0] || 'picker').toLowerCase();
|
|
971
|
+
const scope = hasFlag('--project') ? 'project' : 'global';
|
|
972
|
+
const cwd = safeCwd();
|
|
973
|
+
const installer = new SkillInstaller({ cwd });
|
|
974
|
+
const loader = new SkillsLoader().load(cwd);
|
|
975
|
+
|
|
976
|
+
if (action === 'picker' || action === '') {
|
|
977
|
+
const skills = loader.list({ scope: hasFlag('--all') ? '' : '' });
|
|
978
|
+
if (!skills.length) {
|
|
979
|
+
process.stderr.write(` ${c.dim('No skills installed yet.')}\n`);
|
|
980
|
+
process.stderr.write(` ${c.dim('Install one:')} ${c.brand('/skills install https://github.com/<user>/<repo>')}\n`);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const rl = ctx?.rl || null;
|
|
984
|
+
const choice = await openSkillsPicker({ rl, skills });
|
|
985
|
+
if (!choice) {
|
|
986
|
+
process.stderr.write(formatSkillsList(skills));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (choice.action === 'view') {
|
|
990
|
+
try {
|
|
991
|
+
const view = loader.view(choice.name, null);
|
|
992
|
+
process.stderr.write(`\n ${c.bold(view.name)} ${c.dim(`· ${view.source || ''}`)}\n`);
|
|
993
|
+
if (view.description) process.stderr.write(` ${c.dim(view.description)}\n`);
|
|
994
|
+
process.stderr.write(` ${c.gray('─'.repeat(60))}\n`);
|
|
995
|
+
process.stderr.write(renderMarkdown(String(view.instructions || '')) + '\n');
|
|
996
|
+
if (view.resources?.length) {
|
|
997
|
+
process.stderr.write(` ${c.dim('Bundled resources:')}\n`);
|
|
998
|
+
for (const r of view.resources.slice(0, 20)) process.stderr.write(` ${c.dim('·')} ${r}\n`);
|
|
999
|
+
if (view.resources.length > 20) process.stderr.write(` ${c.dim(`(+${view.resources.length - 20} more)`)}\n`);
|
|
1000
|
+
process.stderr.write(` ${c.dim('View one:')} /skills view ${view.name} <path>\n`);
|
|
1001
|
+
}
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (choice.action === 'remove') {
|
|
1008
|
+
try {
|
|
1009
|
+
installer.remove(choice.name, { scope: 'global' });
|
|
1010
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Removed skill:')} ${choice.name}\n`);
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
// Try project scope if global failed.
|
|
1013
|
+
try {
|
|
1014
|
+
installer.remove(choice.name, { scope: 'project' });
|
|
1015
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Removed skill:')} ${choice.name} ${c.dim('(project)')}\n`);
|
|
1016
|
+
} catch (err2) {
|
|
1017
|
+
process.stderr.write(` ${c.red(err2.message || String(err2))}\n`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (action === 'list' || action === 'ls') {
|
|
1026
|
+
const filter = hasFlag('--all') ? '' : scope;
|
|
1027
|
+
const skills = loader.list({ scope: filter });
|
|
1028
|
+
process.stderr.write(formatSkillsList(skills));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (action === 'install') {
|
|
1033
|
+
const source = positional[1];
|
|
1034
|
+
if (!source) {
|
|
1035
|
+
process.stderr.write(` ${c.yellow('Usage:')} /skills install <git-url|path> [--project] [--force]\n`);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
process.stderr.write(` ${c.dim('Installing')} ${source} ${c.dim(`→ ${scope}`)}...\n`);
|
|
1040
|
+
const result = installer.install(source, { scope, force: hasFlag('--force') });
|
|
1041
|
+
const unique = [...new Set(result.installed || [])];
|
|
1042
|
+
const dupes = (result.installed?.length || 0) - unique.length;
|
|
1043
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Installed:')} ${unique.join(', ') || '(none)'}\n`);
|
|
1044
|
+
if (dupes > 0) {
|
|
1045
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Source shipped ${dupes} duplicate SKILL.md file(s) with the same name — last one won.`)}\n`);
|
|
1046
|
+
}
|
|
1047
|
+
process.stderr.write(` ${c.dim('Lock file:')} ${result.lock_file}\n`);
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (action === 'view') {
|
|
1055
|
+
const name = positional[1];
|
|
1056
|
+
if (!name) {
|
|
1057
|
+
process.stderr.write(` ${c.yellow('Usage:')} /skills view <name> [resource-path]\n`);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const resource = positional[2] || null;
|
|
1061
|
+
try {
|
|
1062
|
+
const view = loader.view(name, resource);
|
|
1063
|
+
process.stderr.write(`\n ${c.bold(view.name)}${resource ? c.dim(` · ${resource}`) : ''}\n`);
|
|
1064
|
+
if (!resource && view.description) process.stderr.write(` ${c.dim(view.description)}\n`);
|
|
1065
|
+
process.stderr.write(` ${c.gray('─'.repeat(60))}\n`);
|
|
1066
|
+
const body = resource ? view.content : view.instructions;
|
|
1067
|
+
process.stderr.write(renderMarkdown(String(body || '')) + '\n');
|
|
1068
|
+
if (!resource && view.resources?.length) {
|
|
1069
|
+
process.stderr.write(` ${c.dim('Bundled resources:')}\n`);
|
|
1070
|
+
for (const r of view.resources.slice(0, 20)) process.stderr.write(` ${c.dim('·')} ${r}\n`);
|
|
1071
|
+
if (view.resources.length > 20) process.stderr.write(` ${c.dim(`(+${view.resources.length - 20} more)`)}\n`);
|
|
1072
|
+
}
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (action === 'remove' || action === 'rm' || action === 'uninstall') {
|
|
1080
|
+
const name = positional[1];
|
|
1081
|
+
if (!name) {
|
|
1082
|
+
process.stderr.write(` ${c.yellow('Usage:')} /skills remove <name> [--project]\n`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
installer.remove(name, { scope });
|
|
1087
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Removed:')} ${name} ${c.dim(`(${scope})`)}\n`);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1090
|
+
}
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (action === 'update' || action === 'upgrade') {
|
|
1095
|
+
const name = positional[1];
|
|
1096
|
+
if (!name) {
|
|
1097
|
+
process.stderr.write(` ${c.yellow('Usage:')} /skills update <name> [--project]\n`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const result = installer.update(name, { scope });
|
|
1102
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Updated:')} ${name} ${c.dim(`(${scope})`)}\n`);
|
|
1103
|
+
if (result?.commit) process.stderr.write(` ${c.dim('Commit:')} ${result.commit.slice(0, 12)}\n`);
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
printSkillsUsage();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
942
1113
|
function commandCompletions(line) {
|
|
943
1114
|
if (line.startsWith('/help ')) {
|
|
944
1115
|
const topic = line.slice('/help '.length).toLowerCase();
|
|
@@ -1241,6 +1412,16 @@ function foldedSubAgentName(data = {}) {
|
|
|
1241
1412
|
return data?.sub_agent || data?.agent || data?.type || 'sub-agent';
|
|
1242
1413
|
}
|
|
1243
1414
|
|
|
1415
|
+
function subAgentStartingLine(agentType = 'sub-agent') {
|
|
1416
|
+
const normalized = String(agentType || 'sub-agent').toLowerCase();
|
|
1417
|
+
if (normalized === 'plan') return '→ preparing plan handoff';
|
|
1418
|
+
if (normalized === 'explore') return '→ starting exploration';
|
|
1419
|
+
if (normalized === 'verify') return '→ preparing verification';
|
|
1420
|
+
if (normalized === 'debug') return '→ isolating failure context';
|
|
1421
|
+
if (normalized === 'refactor') return '→ preparing refactor pass';
|
|
1422
|
+
return `→ starting ${normalized}`;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1244
1425
|
function ensureFoldedSubAgentTools(agentType) {
|
|
1245
1426
|
const current = runtime.foldedSubAgentTools;
|
|
1246
1427
|
if (current && current.agentType === agentType) return current;
|
|
@@ -1533,11 +1714,19 @@ function renderEvent(event) {
|
|
|
1533
1714
|
const phaseTag = phase === 'mid_turn' ? 'mid-turn' : 'pre-turn';
|
|
1534
1715
|
const collapsed = Number(data?.collapsed_messages || 0);
|
|
1535
1716
|
const kept = Number(data?.kept_recent || 0);
|
|
1536
|
-
|
|
1537
|
-
|
|
1717
|
+
// PRD-213: backend now sends `before_tokens` (real when source is
|
|
1718
|
+
// `ground_truth`, chars/4 estimate when `estimator_fallback`) plus
|
|
1719
|
+
// the resolved threshold. Keep the legacy `before_est_tokens` fall-
|
|
1720
|
+
// back so an older backend that hasn't rolled out yet still renders.
|
|
1721
|
+
const before = Number(data?.before_tokens || data?.before_est_tokens || 0);
|
|
1722
|
+
const threshold = Number(data?.threshold || 160_000);
|
|
1723
|
+
const source = String(data?.source || 'estimator');
|
|
1724
|
+
const tokenLabel = source === 'ground_truth' ? 'real' : 'est';
|
|
1725
|
+
const beforeK = before ? ` · ${Math.round(before / 1000)}k/${Math.round(threshold / 1000)}k ${tokenLabel} tokens` : '';
|
|
1726
|
+
const sourceTag = source === 'ground_truth' ? '' : ` [${source}]`;
|
|
1538
1727
|
const keptPart = kept ? ` · kept last ${kept}` : '';
|
|
1539
1728
|
const preview = String(data?.summary_preview || '').trim();
|
|
1540
|
-
process.stderr.write(` ${c.brand('✎')} ${c.dim(`context summarized (${phaseTag}) · ${collapsed} older message${collapsed === 1 ? '' : 's'} collapsed${keptPart}${beforeK}`)}\n`);
|
|
1729
|
+
process.stderr.write(` ${c.brand('✎')} ${c.dim(`context summarized (${phaseTag}) · ${collapsed} older message${collapsed === 1 ? '' : 's'} collapsed${keptPart}${beforeK}${sourceTag}`)}\n`);
|
|
1541
1730
|
if (preview) {
|
|
1542
1731
|
process.stderr.write(` ${c.dim('› ' + preview)}\n`);
|
|
1543
1732
|
}
|
|
@@ -1780,6 +1969,7 @@ function renderEvent(event) {
|
|
|
1780
1969
|
// Phase per sub-agent run: the status line counts elapsed time and
|
|
1781
1970
|
// tool calls live ("plan agent · 4 calls · 32s") for the whole run.
|
|
1782
1971
|
startSpinner(`${agentType} agent`, { phase: `sub:${agentType}:${Date.now()}` });
|
|
1972
|
+
pushSubAgentWindowLine(subAgentStartingLine(agentType));
|
|
1783
1973
|
break;
|
|
1784
1974
|
}
|
|
1785
1975
|
|
|
@@ -1829,7 +2019,12 @@ function renderEvent(event) {
|
|
|
1829
2019
|
flushFoldedSubAgentTools();
|
|
1830
2020
|
const agentType = data?.type || 'sub-agent';
|
|
1831
2021
|
const usage = data?.usage || {};
|
|
1832
|
-
|
|
2022
|
+
// Output tokens = generation size. Summing input+output across a
|
|
2023
|
+
// multi-iteration sub-agent double-counts the context re-shipped each
|
|
2024
|
+
// iteration (a 16-iter run can inflate to 600k+ "tokens" of which
|
|
2025
|
+
// ~95% is repeated context) — the resulting number reads as usage but
|
|
2026
|
+
// it's really billing accumulation, not useful signal in the close line.
|
|
2027
|
+
const tokens = usage.output_tokens || 0;
|
|
1833
2028
|
const costUsd = usage.cost_usd ?? usage.total_cost_usd ?? data?.cost_usd ?? null;
|
|
1834
2029
|
if (typeof costUsd === 'number') session.savedUsd += costUsd;
|
|
1835
2030
|
const summary = data?.result_summary
|
|
@@ -1869,6 +2064,150 @@ function renderEvent(event) {
|
|
|
1869
2064
|
session.id = data.session_id;
|
|
1870
2065
|
// Track in session manager so conversations save to the right file
|
|
1871
2066
|
if (sessionMgrRef.current) sessionMgrRef.current.setSessionInfo({ session_id: data.session_id });
|
|
2067
|
+
// Wire socket server for attach clients.
|
|
2068
|
+
// renderEvent() is NOT async, so we can't `await startSocketServer(...)`
|
|
2069
|
+
// directly (that fails at import time — "Unexpected reserved word").
|
|
2070
|
+
// Fire-and-forget IIFE, and guard against re-entry on session_info
|
|
2071
|
+
// repeats (backend can re-emit on reconnect; two listen()s on the
|
|
2072
|
+
// same sock path → EADDRINUSE and both server + tap explode).
|
|
2073
|
+
if (process.env.BAHULAM_DAEMON_EVENTLOG === '1' && !session._prd092SocketStarting && !session._prd092SocketServer) {
|
|
2074
|
+
session._prd092SocketStarting = true;
|
|
2075
|
+
const _sid = session.id;
|
|
2076
|
+
(async () => {
|
|
2077
|
+
try {
|
|
2078
|
+
const server = await startSocketServer({
|
|
2079
|
+
sessionId: _sid,
|
|
2080
|
+
onCommand: {
|
|
2081
|
+
// Slice E — approve/deny now resolve any pending approval
|
|
2082
|
+
// registered via interceptApproval(). Returns false if the
|
|
2083
|
+
// apr_id is unknown or already answered (log-only, not an
|
|
2084
|
+
// error surfaced to the client — the racy nature of two
|
|
2085
|
+
// attaches answering is expected).
|
|
2086
|
+
approve: async (payload, attachId) => {
|
|
2087
|
+
const ok = resolvePending('approve', payload?.apr_id, attachId, payload?.note);
|
|
2088
|
+
if (!ok) try { process.stderr.write(`[prd-092] approve for unknown apr_id ${payload?.apr_id}\n`); } catch {}
|
|
2089
|
+
},
|
|
2090
|
+
deny: async (payload, attachId) => {
|
|
2091
|
+
const ok = resolvePending('deny', payload?.apr_id, attachId, payload?.note);
|
|
2092
|
+
if (!ok) try { process.stderr.write(`[prd-092] deny for unknown apr_id ${payload?.apr_id}\n`); } catch {}
|
|
2093
|
+
},
|
|
2094
|
+
// Slice C/E — interrupt from an attach holder cancels the
|
|
2095
|
+
// current turn. Uses the stream client on the closure
|
|
2096
|
+
// above (streamClient variable is in scope in this file
|
|
2097
|
+
// at the outer REPL loop; if unavailable, log and skip).
|
|
2098
|
+
interrupt: async (_payload, _attachId) => {
|
|
2099
|
+
try { if (typeof streamClient?.cancel === 'function') streamClient.cancel(); } catch {}
|
|
2100
|
+
},
|
|
2101
|
+
send_message: async (_payload, _attachId) => {
|
|
2102
|
+
// Slice C follow-up. Requires plumbing into the turn
|
|
2103
|
+
// handler; enqueued as a TODO for the daemon slice.
|
|
2104
|
+
},
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
session._prd092SocketServer = server;
|
|
2108
|
+
session._prd092Unregister = registerBroadcaster(evt => server.broadcastEvent(evt));
|
|
2109
|
+
// Slice E — input-lock changes broadcast via the tap so the
|
|
2110
|
+
// input_lock_changed events flow to attached clients + land
|
|
2111
|
+
// in the event log. Emit under the sessionId in scope.
|
|
2112
|
+
wireInputLockEmit((type, data) => {
|
|
2113
|
+
try { tapSseEvent({ type, data }, { sessionId: _sid }); }
|
|
2114
|
+
catch { /* never blocks a lock transition */ }
|
|
2115
|
+
});
|
|
2116
|
+
// Slice E — apply the timeout policy from env (safe default:
|
|
2117
|
+
// 'hold' = never times out). Explicit opt-in via env var so
|
|
2118
|
+
// running unattended is a conscious choice, not a surprise.
|
|
2119
|
+
// BAHULAM_APPROVAL_TIMEOUT=hold (default)
|
|
2120
|
+
// BAHULAM_APPROVAL_TIMEOUT=deny:300 (auto-deny after 5min)
|
|
2121
|
+
// BAHULAM_APPROVAL_TIMEOUT=allow:300 (auto-approve; gated behind --dangerously-skip-permissions elsewhere)
|
|
2122
|
+
try {
|
|
2123
|
+
const p = String(process.env.BAHULAM_APPROVAL_TIMEOUT || 'hold').trim();
|
|
2124
|
+
const [mode, secStr] = p.split(':');
|
|
2125
|
+
setTimeoutPolicy({
|
|
2126
|
+
mode: (mode === 'deny' || mode === 'allow') ? mode : 'hold',
|
|
2127
|
+
durationSec: Number(secStr) || 0,
|
|
2128
|
+
});
|
|
2129
|
+
} catch { /* stay on hold */ }
|
|
2130
|
+
// Slice E — intercept ApprovalManager.check so every prompt
|
|
2131
|
+
// also emits approval_required to attached clients AND can be
|
|
2132
|
+
// resolved by a remote approve/deny command (racing the local
|
|
2133
|
+
// TTY prompt; first answer wins). Only patch once per session.
|
|
2134
|
+
try {
|
|
2135
|
+
if (approval && !approval._prd092Intercepted) {
|
|
2136
|
+
const orig = approval.check.bind(approval);
|
|
2137
|
+
approval.check = (tool, args, req, ctx) => interceptApproval(orig, {
|
|
2138
|
+
tool, args, req, ctx,
|
|
2139
|
+
sessionId: _sid,
|
|
2140
|
+
emit: (type, data) => {
|
|
2141
|
+
try { tapSseEvent({ type, data }, { sessionId: _sid }); }
|
|
2142
|
+
catch { /* never blocks approval */ }
|
|
2143
|
+
},
|
|
2144
|
+
});
|
|
2145
|
+
approval._prd092Intercepted = true;
|
|
2146
|
+
}
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
try { process.stderr.write(`[prd-092] approval intercept: ${err.message}\n`); } catch {}
|
|
2149
|
+
}
|
|
2150
|
+
// Slice H — dial the relay if `bahulam remote enable` set the
|
|
2151
|
+
// flag. Bridge is bidirectional: local events go out as
|
|
2152
|
+
// control-frame envelopes, incoming envelopes dispatch to the
|
|
2153
|
+
// same handlers the local socket-server uses (so approve/deny
|
|
2154
|
+
// from a phone runs the same resolvePending path).
|
|
2155
|
+
try {
|
|
2156
|
+
const remoteCfg = loadRemoteConfig();
|
|
2157
|
+
if (remoteCfg?.enabled) {
|
|
2158
|
+
const bridge = startRelayBridge({
|
|
2159
|
+
sessionId: _sid,
|
|
2160
|
+
remoteConfig: remoteCfg,
|
|
2161
|
+
registerBroadcaster,
|
|
2162
|
+
onCommand: {
|
|
2163
|
+
approve: async (payload, attachId) => resolvePending('approve', payload?.apr_id, attachId, payload?.note),
|
|
2164
|
+
deny: async (payload, attachId) => resolvePending('deny', payload?.apr_id, attachId, payload?.note),
|
|
2165
|
+
interrupt: async () => { try { if (typeof streamClient?.cancel === 'function') streamClient.cancel(); } catch {} },
|
|
2166
|
+
send_message: async (_p, _a) => { /* Slice C follow-up */ },
|
|
2167
|
+
},
|
|
2168
|
+
});
|
|
2169
|
+
session._prd092RelayBridge = bridge;
|
|
2170
|
+
}
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
try { process.stderr.write(`[prd-092] relay bridge: ${err.message}\n`); } catch {}
|
|
2173
|
+
}
|
|
2174
|
+
// Populate ~/.bahulam/sessions/<id>/meta.json + daemon.pid so
|
|
2175
|
+
// `bahulam list` and `bahulam stop <id>` see this session.
|
|
2176
|
+
// These files are what session-list.mjs / stop-daemon.mjs
|
|
2177
|
+
// read; without them those commands are cosmetic. Fire-and-
|
|
2178
|
+
// forget imports so a write failure never affects the turn.
|
|
2179
|
+
try {
|
|
2180
|
+
const [{ writeSessionMeta }, fs, path, { daemonSessionDir }] = await Promise.all([
|
|
2181
|
+
import('../core/event-log.mjs'),
|
|
2182
|
+
import('node:fs'),
|
|
2183
|
+
import('node:path'),
|
|
2184
|
+
import('../core/paths.mjs'),
|
|
2185
|
+
]);
|
|
2186
|
+
await writeSessionMeta({
|
|
2187
|
+
sessionId: _sid,
|
|
2188
|
+
meta: {
|
|
2189
|
+
cwd: process.cwd(),
|
|
2190
|
+
model: session.model || null,
|
|
2191
|
+
pid: process.pid,
|
|
2192
|
+
sock_path: server.sockPath,
|
|
2193
|
+
opened_at: new Date().toISOString(),
|
|
2194
|
+
},
|
|
2195
|
+
});
|
|
2196
|
+
fs.writeFileSync(
|
|
2197
|
+
path.join(daemonSessionDir(_sid), 'daemon.pid'),
|
|
2198
|
+
String(process.pid),
|
|
2199
|
+
{ mode: 0o600 },
|
|
2200
|
+
);
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
try { process.stderr.write(`[prd-092] meta/pid write: ${err.message}\n`); } catch {}
|
|
2203
|
+
}
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
try { process.stderr.write(`[prd-092] socket server: ${err.message}\n`); } catch {}
|
|
2206
|
+
} finally {
|
|
2207
|
+
session._prd092SocketStarting = false;
|
|
2208
|
+
}
|
|
2209
|
+
})();
|
|
2210
|
+
}
|
|
1872
2211
|
}
|
|
1873
2212
|
if (data?.model) session.model = data.model;
|
|
1874
2213
|
if (data?.models?.coder) session.model = data.models.coder;
|
|
@@ -3240,9 +3579,14 @@ async function handleCommand(input, ctx) {
|
|
|
3240
3579
|
}
|
|
3241
3580
|
|
|
3242
3581
|
case '/agents':
|
|
3582
|
+
case '/subagents':
|
|
3243
3583
|
await handleAgentsCommand(rest, ctx);
|
|
3244
3584
|
return;
|
|
3245
3585
|
|
|
3586
|
+
case '/skills':
|
|
3587
|
+
await handleSkillsCommand(rest, ctx);
|
|
3588
|
+
return;
|
|
3589
|
+
|
|
3246
3590
|
case '/explore':
|
|
3247
3591
|
case '/review':
|
|
3248
3592
|
case '/architect': {
|
|
@@ -3398,7 +3742,7 @@ export async function startTerminalRepl() {
|
|
|
3398
3742
|
stopSpinner();
|
|
3399
3743
|
flushContent();
|
|
3400
3744
|
flushPendingHead();
|
|
3401
|
-
|
|
3745
|
+
flushExploreRun();
|
|
3402
3746
|
runtime.foldedSubAgentTools = null;
|
|
3403
3747
|
clearCards();
|
|
3404
3748
|
|
|
@@ -4932,8 +5276,47 @@ export async function startTerminalRepl() {
|
|
|
4932
5276
|
}
|
|
4933
5277
|
}
|
|
4934
5278
|
|
|
4935
|
-
|
|
5279
|
+
// PRD-091 Phase 3 preview: BAHULAM_USE_GATEWAY_LOOP=1 routes the
|
|
5280
|
+
// turn through the gateway's /v1/agent/turn (thin CLI loop) instead
|
|
5281
|
+
// of the local bundled runtime. Session is bootstrapped lazily on
|
|
5282
|
+
// first turn and reused across the REPL. Falls through to the
|
|
5283
|
+
// existing local-agent path when the flag is unset (default today).
|
|
5284
|
+
const _useGatewayLoop = process.env.BAHULAM_USE_GATEWAY_LOOP === '1';
|
|
5285
|
+
let _turnIterable;
|
|
5286
|
+
if (_useGatewayLoop) {
|
|
5287
|
+
if (!session.gatewaySession) {
|
|
5288
|
+
try {
|
|
5289
|
+
session.gatewaySession = await createGatewaySession({
|
|
5290
|
+
workspace: process.env.BAHULAM_WORKSPACE || 'kepler-code',
|
|
5291
|
+
model: process.env.BAHULAM_MODEL || undefined,
|
|
5292
|
+
});
|
|
5293
|
+
process.stderr.write(
|
|
5294
|
+
` ${c.dim(`[gateway] session ${session.gatewaySession.session_id.slice(0, 20)}… ${session.gatewaySession.tool_schemas.length} tools, model=${session.gatewaySession.model}`)}\n`,
|
|
5295
|
+
);
|
|
5296
|
+
} catch (err) {
|
|
5297
|
+
process.stderr.write(` ${c.warn(`[gateway] session create failed: ${err.message}`)}\n`);
|
|
5298
|
+
process.stderr.write(` ${c.dim('falling back to local-agent path')}\n`);
|
|
5299
|
+
session.gatewaySession = null; // don't retry every turn
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
if (session.gatewaySession) {
|
|
5303
|
+
_turnIterable = createAgentLoop({
|
|
5304
|
+
session: session.gatewaySession,
|
|
5305
|
+
messages: session.agentHistory,
|
|
5306
|
+
toolExecutor,
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
if (!_turnIterable) {
|
|
5311
|
+
_turnIterable = client.execute(input, execContext, session.agentHistory);
|
|
5312
|
+
}
|
|
5313
|
+
for await (const event of _turnIterable) {
|
|
4936
5314
|
jsonlWriter.writeKeplerEvent(event);
|
|
5315
|
+
// . daemon event log. Env-var gated (off by default) —
|
|
5316
|
+
// when BAHULAM_DAEMON_EVENTLOG=1, mirror each SSE frame that maps
|
|
5317
|
+
// to a first-class type into ~/.bahulam/sessions/<id>/events.jsonl.
|
|
5318
|
+
// No-op otherwise; zero effect on the render path either way.
|
|
5319
|
+
tapSseEvent(event, { sessionId: session.id });
|
|
4937
5320
|
if (event.type === 'plan_created' || event.type === 'goal_created') {
|
|
4938
5321
|
persistProjectArtifacts(
|
|
4939
5322
|
event.data,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive skills picker — reuses the raw-stdin overlay pattern from
|
|
3
|
+
* repl-ask-form.mjs. Arrow keys move, Enter views the selected SKILL.md,
|
|
4
|
+
* Esc/q closes. Emits nothing when the terminal is not a TTY (falls
|
|
5
|
+
* back to plain-text listing at the call site).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { c, renderMarkdown, stripAnsi } from './ansi.mjs';
|
|
9
|
+
import { fitAnsiLine, writeOverlayFrame, eraseOverlayFrame } from './repl-format.mjs';
|
|
10
|
+
|
|
11
|
+
function truncate(str, max) {
|
|
12
|
+
const s = String(str || '');
|
|
13
|
+
if (s.length <= max) return s;
|
|
14
|
+
return s.slice(0, Math.max(1, max - 1)) + '…';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {object|null} opts.rl readline instance to pause/resume
|
|
20
|
+
* @param {Array<{name,description,scope,source}>} opts.skills
|
|
21
|
+
* @returns {Promise<{action:'view',name:string}|{action:'remove',name:string}|null>}
|
|
22
|
+
*/
|
|
23
|
+
export async function openSkillsPicker({ rl, skills }) {
|
|
24
|
+
if (!process.stdin.isTTY) return null;
|
|
25
|
+
if (!skills?.length) return null;
|
|
26
|
+
if (rl) rl.pause();
|
|
27
|
+
|
|
28
|
+
const rows = skills.map(s => ({
|
|
29
|
+
name: s.name,
|
|
30
|
+
description: s.description || '',
|
|
31
|
+
scope: s.scope,
|
|
32
|
+
source: s.source,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return await new Promise((resolve) => {
|
|
36
|
+
const wasRaw = process.stdin.isRaw;
|
|
37
|
+
let cursor = 0;
|
|
38
|
+
let renderedLines = 0;
|
|
39
|
+
|
|
40
|
+
const render = () => {
|
|
41
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
42
|
+
const nameWidth = Math.min(24, Math.max(...rows.map(r => r.name.length)));
|
|
43
|
+
const scopeWidth = 8;
|
|
44
|
+
const lines = [];
|
|
45
|
+
lines.push(` ${c.bold('Installed skills')} ${c.dim(`· ${rows.length} bundle${rows.length === 1 ? '' : 's'}`)}`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
rows.forEach((row, i) => {
|
|
48
|
+
const active = i === cursor;
|
|
49
|
+
const marker = active ? c.brand('▸') : ' ';
|
|
50
|
+
const name = row.name.padEnd(nameWidth).slice(0, nameWidth);
|
|
51
|
+
const scope = row.scope.padEnd(scopeWidth).slice(0, scopeWidth);
|
|
52
|
+
const descBudget = Math.max(20, cols - 6 - nameWidth - scopeWidth - 4);
|
|
53
|
+
const desc = truncate(row.description, descBudget);
|
|
54
|
+
const painted = active
|
|
55
|
+
? `${c.brand(name)} ${c.dim(scope)} ${c.brand(desc)}`
|
|
56
|
+
: `${c.bold(name)} ${c.dim(scope)} ${desc}`;
|
|
57
|
+
lines.push(fitAnsiLine(` ${marker} ${painted}`, cols - 1));
|
|
58
|
+
});
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(fitAnsiLine(
|
|
61
|
+
` ${c.dim('↑↓ move · Enter view SKILL.md · r remove · Esc close')}`,
|
|
62
|
+
cols - 1,
|
|
63
|
+
));
|
|
64
|
+
writeOverlayFrame(renderedLines, lines);
|
|
65
|
+
renderedLines = lines.length;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const cleanup = (value) => {
|
|
69
|
+
process.stdin.removeListener('data', onData);
|
|
70
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
71
|
+
eraseOverlayFrame(renderedLines);
|
|
72
|
+
if (rl) rl.resume();
|
|
73
|
+
resolve(value);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onData = (data) => {
|
|
77
|
+
const key = data.toString('utf8');
|
|
78
|
+
if (key === '\x1b' || key === '\x03' || key === 'q') { cleanup(null); return; }
|
|
79
|
+
if (key === '\r' || key === '\n') {
|
|
80
|
+
cleanup({ action: 'view', name: rows[cursor].name });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key === 'r' || key === 'R') {
|
|
84
|
+
cleanup({ action: 'remove', name: rows[cursor].name });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (key === '\x1b[A' || key === 'k') { cursor = Math.max(0, cursor - 1); render(); return; }
|
|
88
|
+
if (key === '\x1b[B' || key === 'j') { cursor = Math.min(rows.length - 1, cursor + 1); render(); return; }
|
|
89
|
+
if (key === '\x1b[H' || key === 'g') { cursor = 0; render(); return; }
|
|
90
|
+
if (key === '\x1b[F' || key === 'G') { cursor = rows.length - 1; render(); return; }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
process.stdin.setRawMode(true);
|
|
94
|
+
process.stdin.resume();
|
|
95
|
+
process.stdin.on('data', onData);
|
|
96
|
+
render();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render a nicely-aligned static list (fallback when no TTY, or for `/skills list`).
|
|
102
|
+
* Returns the printable string; caller writes it to stderr.
|
|
103
|
+
*/
|
|
104
|
+
export function formatSkillsList(skills) {
|
|
105
|
+
if (!skills?.length) return ` ${c.dim('No skills installed. Try /skills install <git-url>')}\n`;
|
|
106
|
+
const nameWidth = Math.min(28, Math.max(...skills.map(s => s.name.length)));
|
|
107
|
+
const scopeWidth = 8;
|
|
108
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
109
|
+
const descBudget = Math.max(20, cols - 6 - nameWidth - scopeWidth - 4);
|
|
110
|
+
const lines = [` ${c.bold('Installed skills')} ${c.dim(`· ${skills.length} bundle${skills.length === 1 ? '' : 's'}`)}\n`];
|
|
111
|
+
for (const s of skills) {
|
|
112
|
+
const name = s.name.padEnd(nameWidth).slice(0, nameWidth);
|
|
113
|
+
const scope = String(s.scope || '').padEnd(scopeWidth).slice(0, scopeWidth);
|
|
114
|
+
const desc = truncate(s.description || '', descBudget);
|
|
115
|
+
lines.push(` ${c.bold(name)} ${c.dim(scope)} ${desc}\n`);
|
|
116
|
+
}
|
|
117
|
+
lines.push(`\n ${c.dim('Open interactively:')} /skills\n`);
|
|
118
|
+
return lines.join('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { renderMarkdown, stripAnsi };
|
package/src/terminal/skills.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SkillInstaller } from '../skills/installer.mjs';
|
|
2
2
|
import { SkillsLoader } from '../skills/loader.mjs';
|
|
3
|
+
import { formatSkillsList } from './skills-picker.mjs';
|
|
3
4
|
|
|
4
5
|
function has(args, flag) {
|
|
5
6
|
return args.includes(flag);
|
|
@@ -22,9 +23,8 @@ export async function runSkillsCommand(args, { cwd = process.cwd() } = {}) {
|
|
|
22
23
|
|
|
23
24
|
if (action === 'list') {
|
|
24
25
|
const rows = loader.list({ scope: has(rest, '--all') ? '' : scope });
|
|
25
|
-
if (has(rest, '--json')) print(rows);
|
|
26
|
-
|
|
27
|
-
else for (const row of rows) print(`${row.name}\t${row.scope}\t${row.source}\t${row.description}`);
|
|
26
|
+
if (has(rest, '--json')) { print(rows); return; }
|
|
27
|
+
process.stdout.write(formatSkillsList(rows));
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
if (action === 'view') {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Code Tool — AST-based structured code analysis (matches Python schema).
|
|
3
|
+
*/
|
|
4
|
+
import { analyzeCode } from '../context/ast-parser.mjs';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const AnalyzeCodeTool = {
|
|
9
|
+
name: 'analyze_code',
|
|
10
|
+
description:
|
|
11
|
+
'Get structured analysis of one specific file: function/class names with LINE NUMBERS, imports, exports. Never pass a directory or project root.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
file_path: { type: 'string', description: 'Path to a specific file to analyze' },
|
|
16
|
+
},
|
|
17
|
+
required: ['file_path'],
|
|
18
|
+
},
|
|
19
|
+
validateInput(input) {
|
|
20
|
+
return input.file_path ? [] : ['file_path required'];
|
|
21
|
+
},
|
|
22
|
+
async call(input) {
|
|
23
|
+
const filePath = path.resolve(input.file_path);
|
|
24
|
+
let stat;
|
|
25
|
+
try {
|
|
26
|
+
stat = fs.statSync(filePath);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return `Error: ${err.message}`;
|
|
29
|
+
}
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
return `Error: analyze_code expects a file, but got directory: ${filePath}. Use list_files/search_code first, then pass a specific source file.`;
|
|
32
|
+
}
|
|
33
|
+
const result = analyzeCode(filePath, {
|
|
34
|
+
startLine: input.start_line,
|
|
35
|
+
endLine: input.end_line,
|
|
36
|
+
});
|
|
37
|
+
return result.summary;
|
|
38
|
+
},
|
|
39
|
+
};
|
package/src/tools/bash.mjs
CHANGED