@bahulam/code 0.1.6 → 0.1.8
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 +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- 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 +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- 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 +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -24,6 +24,8 @@ const TOOL_LABELS = Object.freeze({
|
|
|
24
24
|
git_diff: 'Reviewing changes',
|
|
25
25
|
git_status: 'Checking git',
|
|
26
26
|
analyze_code: 'Analyzing',
|
|
27
|
+
analyze_image: 'Analyzing image',
|
|
28
|
+
generate_image: 'Generating image',
|
|
27
29
|
get_project_overview: 'Indexing project',
|
|
28
30
|
skills_list: 'Listing skills',
|
|
29
31
|
skill_view: 'Loading skill',
|
|
@@ -76,9 +78,39 @@ function currentWorkingDirectory() {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// Shortens a path for tool-card display without dropping the meaningful
|
|
82
|
+
// tail. Strategy, in priority order:
|
|
83
|
+
//
|
|
84
|
+
// 1. INSIDE cwd /repo/foo/bar.py (cwd = /repo) → foo/bar.py
|
|
85
|
+
// 2. SIBLING under cwd's /work/repo-a/x.py (cwd = /work/repo-b)
|
|
86
|
+
// parent → …/repo-a/x.py
|
|
87
|
+
// (typical mono-repo / adjacent-clone case)
|
|
88
|
+
// 3. UNDER $HOME /Users/sree/Sites/…/x.py → ~/Sites/…/x.py
|
|
89
|
+
// 4. Absolute (fallback — printed as-is; card truncator
|
|
90
|
+
// handles the display width from here)
|
|
91
|
+
//
|
|
92
|
+
// The point: keep the basename + parent visible even when the full path
|
|
93
|
+
// is long, so users don't see "Editing /Users/sree/Sites/.../
|
|
94
|
+
// codekepler-ba…" with the actual filename lost off the right edge.
|
|
79
95
|
function shortPath(filePath, cwd = currentWorkingDirectory()) {
|
|
80
96
|
const value = String(filePath || '');
|
|
97
|
+
if (!value) return value;
|
|
98
|
+
|
|
99
|
+
// 1. inside cwd
|
|
81
100
|
if (cwd && value.startsWith(`${cwd}/`)) return value.slice(cwd.length + 1);
|
|
101
|
+
|
|
102
|
+
// 2. sibling under cwd's parent (e.g. sister repo in the same workspace)
|
|
103
|
+
if (cwd) {
|
|
104
|
+
const cwdParent = cwd.slice(0, cwd.lastIndexOf('/'));
|
|
105
|
+
if (cwdParent && value.startsWith(`${cwdParent}/`)) {
|
|
106
|
+
return `…/${value.slice(cwdParent.length + 1)}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. under $HOME
|
|
111
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
112
|
+
if (home && value.startsWith(`${home}/`)) return `~/${value.slice(home.length + 1)}`;
|
|
113
|
+
|
|
82
114
|
return value;
|
|
83
115
|
}
|
|
84
116
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch panel — mutable state for the live agent activity tracker.
|
|
3
|
+
*
|
|
4
|
+
* Singleton exported as `watchState`. /watch toggles `active`, and
|
|
5
|
+
* renderEvent hooks push entries during the agent loop. Capped at
|
|
6
|
+
* MAX_ENTRIES to bound memory.
|
|
7
|
+
*
|
|
8
|
+
* Pattern: same as repl-state.mjs — exported mutable singleton.
|
|
9
|
+
*/
|
|
10
|
+
import { paint } from '../ui/palette.mjs';
|
|
11
|
+
import { c } from './ansi.mjs';
|
|
12
|
+
|
|
13
|
+
const MAX_ENTRIES = 80;
|
|
14
|
+
|
|
15
|
+
/** @typedef {'spawn'|'activate'|'deactivate'|'handoff'|'tool'|'done'|'error'} EntryKind */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} WatchEntry
|
|
19
|
+
* @property {EntryKind} kind
|
|
20
|
+
* @property {string} id — unique within the session
|
|
21
|
+
* @property {string} [type] — agent type (explore, plan, etc.)
|
|
22
|
+
* @property {string} [label] — display label (query, tool name, etc.)
|
|
23
|
+
* @property {number} [elapsed] — ms since entry was created
|
|
24
|
+
* @property {string} [detail] — extra info (model, tool count, error msg)
|
|
25
|
+
* @property {number} createdAt
|
|
26
|
+
* @property {'active'|'done'|'error'} [status]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const watchState = {
|
|
30
|
+
/** /watch toggle — when off, no entries are collected */
|
|
31
|
+
active: false,
|
|
32
|
+
|
|
33
|
+
/** Ordered list of watch entries, newest last */
|
|
34
|
+
entries: /** @type {WatchEntry[]} */ ([]),
|
|
35
|
+
|
|
36
|
+
/** Max visible rows in the panel (terminal rows reserved) */
|
|
37
|
+
maxRows: 6,
|
|
38
|
+
|
|
39
|
+
/** Turn-local counter for generating stable ids */
|
|
40
|
+
_seq: 0,
|
|
41
|
+
|
|
42
|
+
/** Add a new entry (prepends to front, pops if over MAX_ENTRIES) */
|
|
43
|
+
addEntry(kind, opts = {}) {
|
|
44
|
+
if (!this.active) return;
|
|
45
|
+
const id = opts.id || `w${++this._seq}`;
|
|
46
|
+
const entry = {
|
|
47
|
+
kind,
|
|
48
|
+
id,
|
|
49
|
+
type: opts.type || '',
|
|
50
|
+
label: opts.label || '',
|
|
51
|
+
elapsed: opts.elapsed || 0,
|
|
52
|
+
detail: opts.detail || '',
|
|
53
|
+
createdAt: Date.now(),
|
|
54
|
+
status: opts.status || 'active',
|
|
55
|
+
};
|
|
56
|
+
this.entries.push(entry);
|
|
57
|
+
if (this.entries.length > MAX_ENTRIES) {
|
|
58
|
+
this.entries.splice(0, this.entries.length - MAX_ENTRIES);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** Update an entry by id (merge). */
|
|
63
|
+
updateEntry(id, patch) {
|
|
64
|
+
if (!this.active) return;
|
|
65
|
+
const entry = this.entries.find(e => e.id === id);
|
|
66
|
+
if (entry) Object.assign(entry, patch);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/** Remove entries that match a predicate. */
|
|
70
|
+
removeWhere(fn) {
|
|
71
|
+
if (!this.active) return;
|
|
72
|
+
this.entries = this.entries.filter(e => !fn(e));
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
clear() {
|
|
76
|
+
this.entries = [];
|
|
77
|
+
this._seq = 0;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Compact printable entries for the panel (newest last). */
|
|
81
|
+
visible(count = this.maxRows) {
|
|
82
|
+
const slice = this.entries.slice(-count);
|
|
83
|
+
return slice.map(e => renderEntryLine(e));
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Format a single WatchEntry to an ANSI line. */
|
|
88
|
+
function renderEntryLine(entry) {
|
|
89
|
+
const time = entry.elapsed
|
|
90
|
+
? c.dim(` ${entry.elapsed < 1000 ? `${entry.elapsed}ms` : `${(entry.elapsed / 1000).toFixed(1)}s`}`)
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
switch (entry.kind) {
|
|
94
|
+
case 'spawn':
|
|
95
|
+
return ` ${c.green('+')} ${paint.text.dim(entry.type || 'agent')}${entry.label ? ` ${c.dim(entry.label)}` : ''}${time}`;
|
|
96
|
+
|
|
97
|
+
case 'activate':
|
|
98
|
+
return ` ${c.cyan('▸')} ${paint.text.dim(entry.type || 'agent')}${entry.label ? ` ${c.dim(entry.label)}` : ''}${time}`;
|
|
99
|
+
|
|
100
|
+
case 'deactivate':
|
|
101
|
+
return ` ${c.dim('✗')} ${paint.text.dim(entry.type || 'agent')}${entry.detail ? ` ${c.dim(entry.detail)}` : ''}`;
|
|
102
|
+
|
|
103
|
+
case 'handoff':
|
|
104
|
+
return ` ${c.yellow('↳')} ${paint.text.dim(entry.label || 'handoff')}${time}`;
|
|
105
|
+
|
|
106
|
+
case 'tool':
|
|
107
|
+
return ` ${paint.text.dim('·')} ${c.dim(entry.type || 'tool')}${entry.label ? ` ${c.dim(entry.label)}` : ''}${time}`;
|
|
108
|
+
|
|
109
|
+
case 'done':
|
|
110
|
+
return ` ${c.green('✓')} ${paint.text.dim(entry.type || 'agent')}${entry.detail ? ` ${c.dim(entry.detail)}` : ''}${time}`;
|
|
111
|
+
|
|
112
|
+
case 'error':
|
|
113
|
+
return ` ${c.red('✗')} ${paint.text.dim(entry.type || 'agent')}${entry.detail ? ` ${c.dim(entry.detail)}` : ''}`;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
return ` ${c.dim('·')} ${entry.label || ''}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/tools/agent.mjs
CHANGED
|
@@ -87,10 +87,10 @@ export const AgentTool = {
|
|
|
87
87
|
settings: {
|
|
88
88
|
stream: false,
|
|
89
89
|
stagnationDetection: !['0', 'false', 'no', 'off'].includes(
|
|
90
|
-
(process.env.
|
|
90
|
+
(process.env.BAHULAM_STAGNATION_DETECTION ?? '0').toLowerCase(),
|
|
91
91
|
),
|
|
92
92
|
stagnationThreshold: Number.parseInt(
|
|
93
|
-
process.env.
|
|
93
|
+
process.env.BAHULAM_STAGNATION_THRESHOLD
|
|
94
94
|
?? '3',
|
|
95
95
|
10,
|
|
96
96
|
),
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze_image — ask a vision model about a local image file.
|
|
3
|
+
*
|
|
4
|
+
* The CLI owns local filesystem access, so this tool reads the image bytes and
|
|
5
|
+
* sends the same bounded attachment shape used by the preflight vision path to
|
|
6
|
+
* the authenticated backend `/api/vision/analyze` endpoint.
|
|
7
|
+
*/
|
|
8
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
9
|
+
import {
|
|
10
|
+
loadImageAttachment,
|
|
11
|
+
publicAttachmentMetadata,
|
|
12
|
+
} from '../core/attachments.mjs';
|
|
13
|
+
|
|
14
|
+
function visionFailureMessage(message, attachment) {
|
|
15
|
+
const base = String(message || 'Vision analysis failed').trim().replace(/[.。]+$/g, '');
|
|
16
|
+
const lower = base.toLowerCase();
|
|
17
|
+
const guidance = `File was loaded as ${attachment?.mime_type || 'an image'}; do not retry this image with read_attachment.`;
|
|
18
|
+
if (lower.includes('empty analysis') || lower.includes('no text answer') || lower.includes('no visible text answer') || lower.includes('finish_reason=length')) {
|
|
19
|
+
return `${base}. ${guidance} The provider call completed but did not return visible answer text; retry analyze_image with a more specific question or switch the configured vision model.`;
|
|
20
|
+
}
|
|
21
|
+
return `${base}. ${guidance}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const AnalyzeImageTool = {
|
|
25
|
+
name: 'analyze_image',
|
|
26
|
+
description:
|
|
27
|
+
'Answer a specific question about a local image file. Pass path=<local image path> and question=<what to inspect>. Use for screenshots, diagrams, photos, and UI images.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
path: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Local image file path. JPEG, PNG, WebP, and GIF are supported.',
|
|
34
|
+
},
|
|
35
|
+
file_path: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Alias for path.',
|
|
38
|
+
},
|
|
39
|
+
upload_id: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Chat-only image upload id. Not supported in the CLI; use path instead.',
|
|
42
|
+
},
|
|
43
|
+
question: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Specific question to ask about the image. Example: "Describe the UI layout and visible errors."',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['question'],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
validateInput(input) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const pathValue = String(input?.path || input?.file_path || '').trim();
|
|
54
|
+
const uploadId = String(input?.upload_id || '').trim();
|
|
55
|
+
if (!pathValue && !uploadId) errors.push('path is required');
|
|
56
|
+
if (!String(input?.question || '').trim()) errors.push('question is required');
|
|
57
|
+
return errors;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async call(input) {
|
|
61
|
+
const uploadId = String(input?.upload_id || '').trim();
|
|
62
|
+
const rawPath = String(input?.path || input?.file_path || '').trim();
|
|
63
|
+
const question = String(input?.question || '').trim() || 'Describe this image in detail.';
|
|
64
|
+
|
|
65
|
+
if (uploadId && !rawPath) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
output: 'upload_id is a chat-only mode. In the CLI, pass path=<local image path>.',
|
|
69
|
+
_tool: 'analyze_image',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!rawPath) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
output: 'analyze_image requires path=<local image path>.',
|
|
76
|
+
_tool: 'analyze_image',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let attachment;
|
|
81
|
+
try {
|
|
82
|
+
attachment = loadImageAttachment(rawPath, { cwd: process.cwd() });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
output: `Failed to load image: ${err.message || err}`,
|
|
87
|
+
_tool: 'analyze_image',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
92
|
+
if (!creds.backendUrl || !creds.token) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
output: 'analyze_image requires CLI auth. Run `bahulam-code login` or set B0_TOKEN/KEPLER_TOKEN.',
|
|
96
|
+
_tool: 'analyze_image',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(`${creds.backendUrl}/api/vision/analyze`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${creds.token}`,
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'X-Product': process.env.BAHULAM_PRODUCT || 'bahulam',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
instruction: question,
|
|
111
|
+
attachments: [attachment],
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
output: `Vision analysis unreachable: ${err.message || err}`,
|
|
118
|
+
_tool: 'analyze_image',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const text = await response.text().catch(() => '');
|
|
123
|
+
let payload = null;
|
|
124
|
+
try {
|
|
125
|
+
payload = text ? JSON.parse(text) : null;
|
|
126
|
+
} catch {
|
|
127
|
+
payload = { detail: text };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const detail = payload?.detail || payload || {};
|
|
132
|
+
const message = typeof detail === 'string'
|
|
133
|
+
? detail
|
|
134
|
+
: detail.message || detail.error || `Vision analysis failed (${response.status})`;
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
output: visionFailureMessage(message, attachment),
|
|
138
|
+
status: response.status,
|
|
139
|
+
detail,
|
|
140
|
+
_tool: 'analyze_image',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const summary = String(payload?.summary || '').trim();
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
output: summary || '(no vision summary returned)',
|
|
148
|
+
summary,
|
|
149
|
+
model: payload?.model || '',
|
|
150
|
+
provider: payload?.provider || 'backend-vision',
|
|
151
|
+
attachments: Array.isArray(payload?.attachments)
|
|
152
|
+
? payload.attachments
|
|
153
|
+
: [publicAttachmentMetadata(attachment)],
|
|
154
|
+
usage: payload?.usage || {},
|
|
155
|
+
_tool: 'analyze_image',
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
};
|
package/src/tools/bash.mjs
CHANGED
|
@@ -18,6 +18,7 @@ function stripAnsi(str) {
|
|
|
18
18
|
|
|
19
19
|
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB
|
|
20
20
|
const TIMEOUT_TAIL_BYTES = 64 * 1024;
|
|
21
|
+
const TRUNCATION_MARKER = '\n[output truncated at 1MB]';
|
|
21
22
|
|
|
22
23
|
export const BashTool = {
|
|
23
24
|
name: 'shell',
|
|
@@ -55,6 +56,8 @@ export const BashTool = {
|
|
|
55
56
|
let stderr = '';
|
|
56
57
|
let stdoutTail = '';
|
|
57
58
|
let stderrTail = '';
|
|
59
|
+
let stdoutTruncated = false;
|
|
60
|
+
let stderrTruncated = false;
|
|
58
61
|
let killed = false;
|
|
59
62
|
let cancelled = false;
|
|
60
63
|
let exitCode = null;
|
|
@@ -98,13 +101,17 @@ export const BashTool = {
|
|
|
98
101
|
|
|
99
102
|
proc.stdout.on('data', (chunk) => {
|
|
100
103
|
const text = chunk.toString();
|
|
101
|
-
|
|
104
|
+
const next = appendHead(stdout, text, MAX_OUTPUT_BYTES);
|
|
105
|
+
if (next.length < stdout.length + text.length) stdoutTruncated = true;
|
|
106
|
+
stdout = next;
|
|
102
107
|
stdoutTail = appendTail(stdoutTail, text, TIMEOUT_TAIL_BYTES);
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
proc.stderr.on('data', (chunk) => {
|
|
106
111
|
const text = chunk.toString();
|
|
107
|
-
|
|
112
|
+
const next = appendHead(stderr, text, MAX_OUTPUT_BYTES);
|
|
113
|
+
if (next.length < stderr.length + text.length) stderrTruncated = true;
|
|
114
|
+
stderr = next;
|
|
108
115
|
stderrTail = appendTail(stderrTail, text, TIMEOUT_TAIL_BYTES);
|
|
109
116
|
});
|
|
110
117
|
|
|
@@ -116,13 +123,8 @@ export const BashTool = {
|
|
|
116
123
|
proc.on('close', (code) => {
|
|
117
124
|
exitCode = code;
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
stdout = stdout.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
|
|
122
|
-
}
|
|
123
|
-
if (stderr.length > MAX_OUTPUT_BYTES) {
|
|
124
|
-
stderr = stderr.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated at 1MB]';
|
|
125
|
-
}
|
|
126
|
+
if (stdoutTruncated) stdout += TRUNCATION_MARKER;
|
|
127
|
+
if (stderrTruncated) stderr += TRUNCATION_MARKER;
|
|
126
128
|
|
|
127
129
|
// Strip ANSI by default
|
|
128
130
|
stdout = stripAnsi(stdout);
|