protoagent 0.0.4 → 0.1.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.
Files changed (58) hide show
  1. package/README.md +99 -19
  2. package/dist/App.js +602 -0
  3. package/dist/agentic-loop.js +492 -525
  4. package/dist/cli.js +39 -0
  5. package/dist/components/CollapsibleBox.js +26 -0
  6. package/dist/components/ConfigDialog.js +40 -0
  7. package/dist/components/ConsolidatedToolMessage.js +41 -0
  8. package/dist/components/FormattedMessage.js +93 -0
  9. package/dist/components/Table.js +275 -0
  10. package/dist/config.js +171 -0
  11. package/dist/mcp.js +170 -0
  12. package/dist/providers.js +137 -0
  13. package/dist/sessions.js +161 -0
  14. package/dist/skills.js +229 -0
  15. package/dist/sub-agent.js +103 -0
  16. package/dist/system-prompt.js +131 -0
  17. package/dist/tools/bash.js +178 -0
  18. package/dist/tools/edit-file.js +65 -171
  19. package/dist/tools/index.js +79 -134
  20. package/dist/tools/list-directory.js +20 -73
  21. package/dist/tools/read-file.js +57 -101
  22. package/dist/tools/search-files.js +74 -162
  23. package/dist/tools/todo.js +57 -140
  24. package/dist/tools/webfetch.js +310 -0
  25. package/dist/tools/write-file.js +44 -135
  26. package/dist/utils/approval.js +69 -0
  27. package/dist/utils/compactor.js +87 -0
  28. package/dist/utils/cost-tracker.js +26 -81
  29. package/dist/utils/format-message.js +26 -0
  30. package/dist/utils/logger.js +101 -307
  31. package/dist/utils/path-validation.js +74 -0
  32. package/package.json +45 -51
  33. package/LICENSE +0 -21
  34. package/dist/config/client.js +0 -315
  35. package/dist/config/commands.js +0 -223
  36. package/dist/config/manager.js +0 -117
  37. package/dist/config/mcp-commands.js +0 -266
  38. package/dist/config/mcp-manager.js +0 -240
  39. package/dist/config/mcp-types.js +0 -28
  40. package/dist/config/providers.js +0 -229
  41. package/dist/config/setup.js +0 -209
  42. package/dist/config/system-prompt.js +0 -397
  43. package/dist/config/types.js +0 -4
  44. package/dist/index.js +0 -222
  45. package/dist/tools/create-directory.js +0 -76
  46. package/dist/tools/directory-operations.js +0 -195
  47. package/dist/tools/file-operations.js +0 -211
  48. package/dist/tools/run-shell-command.js +0 -746
  49. package/dist/tools/search-operations.js +0 -179
  50. package/dist/tools/shell-operations.js +0 -342
  51. package/dist/tools/task-complete.js +0 -26
  52. package/dist/tools/view-directory-tree.js +0 -125
  53. package/dist/tools.js +0 -2
  54. package/dist/utils/conversation-compactor.js +0 -140
  55. package/dist/utils/enhanced-prompt.js +0 -23
  56. package/dist/utils/file-operations-approval.js +0 -373
  57. package/dist/utils/interrupt-handler.js +0 -127
  58. package/dist/utils/user-cancellation.js +0 -34
package/dist/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * CLI entry point for ProtoAgent.
5
+ *
6
+ * Parses command-line flags and launches either the main chat UI
7
+ * or the configuration wizard.
8
+ */
9
+ import process from 'node:process';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { readFileSync } from 'node:fs';
13
+ import { render } from 'ink';
14
+ import { Command } from 'commander';
15
+ import { App } from './App.js';
16
+ import { ConfigureComponent } from './config.js';
17
+ // Get package.json version
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
21
+ const program = new Command();
22
+ program
23
+ .description('ProtoAgent — a simple, hackable coding agent CLI')
24
+ .version(packageJson.version)
25
+ .option('--dangerously-accept-all', 'Auto-approve all file writes and shell commands')
26
+ .option('--log-level <level>', 'Log level: TRACE, DEBUG, INFO, WARN, ERROR', 'INFO')
27
+ .option('--session <id>', 'Resume a previous session by ID')
28
+ .action((options) => {
29
+ // Default action - start the main app
30
+ render(_jsx(App, { dangerouslyAcceptAll: options.dangerouslyAcceptAll || false, logLevel: options.logLevel, sessionId: options.session }));
31
+ });
32
+ // Configure subcommand
33
+ program
34
+ .command('configure')
35
+ .description('Configure AI model and API key settings')
36
+ .action(() => {
37
+ render(_jsx(ConfigureComponent, {}));
38
+ });
39
+ program.parse(process.argv);
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export const CollapsibleBox = ({ title, content, titleColor, dimColor = false, maxPreviewLines = 3, maxPreviewChars = 500, expanded = false, marginBottom = 0, }) => {
4
+ const lines = content.split('\n');
5
+ const isTooManyLines = lines.length > maxPreviewLines;
6
+ const isTooManyChars = content.length > maxPreviewChars;
7
+ const isLong = isTooManyLines || isTooManyChars;
8
+ // If content is short, always show it
9
+ if (!isLong) {
10
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: titleColor, dimColor: dimColor, bold: true, children: title }) }), _jsx(Box, { marginLeft: 2, paddingRight: 1, children: _jsx(Text, { dimColor: dimColor, children: content }) })] }));
11
+ }
12
+ // For long content, show preview or full content
13
+ let preview;
14
+ if (expanded) {
15
+ preview = content;
16
+ }
17
+ else {
18
+ // Truncate by lines first, then by characters
19
+ const linesTruncated = lines.slice(0, maxPreviewLines).join('\n');
20
+ preview = linesTruncated.length > maxPreviewChars
21
+ ? linesTruncated.slice(0, maxPreviewChars)
22
+ : linesTruncated;
23
+ }
24
+ const hasMore = !expanded;
25
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, dimColor: dimColor, bold: true, children: [expanded ? '▼' : '▶', " ", title] }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, paddingRight: 1, children: [_jsx(Text, { dimColor: dimColor, children: preview }), hasMore && _jsx(Text, { dimColor: true, children: "... (use /expand to see all)" })] })] }));
26
+ };
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ConfigDialog — Modal-like dialog for changing config mid-conversation
4
+ *
5
+ * Allows users to update provider, model, or API key without losing chat history.
6
+ */
7
+ import { useState } from 'react';
8
+ import { Box, Text } from 'ink';
9
+ import { PasswordInput, Select } from '@inkjs/ui';
10
+ import { getProvider, SUPPORTED_MODELS } from '../providers.js';
11
+ export const ConfigDialog = ({ currentConfig, onComplete, onCancel, }) => {
12
+ const [step, setStep] = useState('select_provider');
13
+ const [selectedProviderId, setSelectedProviderId] = useState(currentConfig.provider);
14
+ const [selectedModelId, setSelectedModelId] = useState(currentConfig.model);
15
+ const providerItems = SUPPORTED_MODELS.flatMap((provider) => provider.models.map((model) => ({
16
+ label: `${provider.name} - ${model.name}`,
17
+ value: `${provider.id}:::${model.id}`,
18
+ })));
19
+ const currentProvider = getProvider(currentConfig.provider);
20
+ // Provider selection step
21
+ if (step === 'select_provider') {
22
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Change Configuration" }), _jsxs(Text, { dimColor: true, children: ["Current: ", currentProvider?.name, " / ", currentConfig.model] }), _jsx(Text, { dimColor: true, children: "Select a new provider and model:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: providerItems.map((item) => ({ value: item.value, label: item.label })), onChange: (value) => {
23
+ const [providerId, modelId] = value.split(':::');
24
+ setSelectedProviderId(providerId);
25
+ setSelectedModelId(modelId);
26
+ setStep('enter_api_key');
27
+ } }) })] }));
28
+ }
29
+ // API key entry step
30
+ const provider = getProvider(selectedProviderId);
31
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Confirm Configuration" }), _jsxs(Text, { dimColor: true, children: ["Provider: ", provider?.name, " / ", selectedModelId] }), _jsx(Text, { children: "Enter your API key (or leave empty to keep current/env var):" }), _jsx(PasswordInput, { placeholder: `Paste your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: (value) => {
32
+ const finalApiKey = value.trim().length > 0 ? value.trim() : currentConfig.apiKey;
33
+ const newConfig = {
34
+ provider: selectedProviderId,
35
+ model: selectedModelId,
36
+ apiKey: finalApiKey,
37
+ };
38
+ onComplete(newConfig);
39
+ } })] }));
40
+ };
@@ -0,0 +1,41 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Table } from './Table.js';
4
+ import { FormattedMessage } from './FormattedMessage.js';
5
+ export const ConsolidatedToolMessage = ({ toolCalls, toolResults, expanded = false, }) => {
6
+ const toolNames = toolCalls.map((toolCall) => toolCall.name);
7
+ const title = `Called: ${toolNames.join(', ')}`;
8
+ const containsTodoTool = toolCalls.some((toolCall) => toolCall.name === 'todo_read' || toolCall.name === 'todo_write');
9
+ const titleColor = containsTodoTool ? 'green' : 'white';
10
+ const isExpanded = expanded || containsTodoTool;
11
+ if (isExpanded) {
12
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "white", children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, bold: true, children: ["\u25BC ", title] }) }), _jsx(Box, { flexDirection: "column", marginLeft: 2, paddingRight: 1, children: toolCalls.map((toolCall, idx) => {
13
+ const result = toolResults.get(toolCall.id);
14
+ if (!result)
15
+ return null;
16
+ // Try to see if it's JSON that could be a table
17
+ let isJsonTable = false;
18
+ try {
19
+ const parsed = JSON.parse(result.content);
20
+ isJsonTable = typeof parsed === 'object' && parsed !== null;
21
+ }
22
+ catch (e) { }
23
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["[", result.name, "]:"] }), isJsonTable ? (_jsx(Table, { data: result.content })) : (_jsx(FormattedMessage, { content: result.content }))] }, idx));
24
+ }) })] }));
25
+ }
26
+ const compactLines = toolCalls.flatMap((toolCall) => {
27
+ const result = toolResults.get(toolCall.id);
28
+ if (!result)
29
+ return [];
30
+ const compactContent = result.content
31
+ .replace(/\s+/g, ' ')
32
+ .trim();
33
+ return [`[${result.name}] ${compactContent}`];
34
+ });
35
+ const compactPreview = compactLines.join(' | ');
36
+ const previewLimit = 180;
37
+ const preview = compactPreview.length > previewLimit
38
+ ? `${compactPreview.slice(0, previewLimit).trimEnd()}... (use /expand)`
39
+ : compactPreview;
40
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, dimColor: true, bold: true, children: ["\u25B6 ", title] }) }), _jsx(Box, { marginLeft: 2, paddingRight: 1, children: _jsx(Text, { dimColor: true, children: preview }) })] }));
41
+ };
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Table } from './Table.js';
4
+ import { formatMessage } from '../utils/format-message.js';
5
+ /**
6
+ * FormattedMessage component
7
+ *
8
+ * Parses a markdown string and renders:
9
+ * - Standard text with ANSI formatting
10
+ * - Markdown tables using ink-table
11
+ * - Code blocks (rendered in a box)
12
+ */
13
+ export const FormattedMessage = ({ content }) => {
14
+ if (!content)
15
+ return null;
16
+ const lines = content.split('\n');
17
+ const blocks = [];
18
+ let currentBlockContent = [];
19
+ let currentBlockType = 'text';
20
+ for (let i = 0; i < lines.length; i++) {
21
+ const line = lines[i];
22
+ const trimmedLine = line.trim();
23
+ // 1. Handle Code Blocks
24
+ if (currentBlockType === 'code') {
25
+ currentBlockContent.push(line);
26
+ // Check for end of code block
27
+ if (trimmedLine.startsWith('```')) {
28
+ blocks.push({ type: 'code', content: currentBlockContent.join('\n') });
29
+ currentBlockContent = [];
30
+ currentBlockType = 'text';
31
+ }
32
+ continue;
33
+ }
34
+ // Start Code Block
35
+ if (trimmedLine.startsWith('```')) {
36
+ // Finish pending text block
37
+ if (currentBlockContent.length > 0) {
38
+ blocks.push({ type: 'text', content: currentBlockContent.join('\n') });
39
+ }
40
+ currentBlockContent = [line];
41
+ currentBlockType = 'code';
42
+ continue;
43
+ }
44
+ // 2. Handle Tables
45
+ if (currentBlockType === 'table') {
46
+ if (trimmedLine.startsWith('|')) {
47
+ currentBlockContent.push(line);
48
+ continue;
49
+ }
50
+ else {
51
+ // End of table block found (line doesn't start with |)
52
+ blocks.push({ type: 'table', content: currentBlockContent.join('\n') });
53
+ // Reset to text and fall through to re-process this line
54
+ currentBlockContent = [];
55
+ currentBlockType = 'text';
56
+ }
57
+ }
58
+ // Start Table Block check
59
+ // A table start requires a pipe AND a subsequent separator line
60
+ const isTableStart = trimmedLine.startsWith('|');
61
+ const nextLine = lines[i + 1];
62
+ const isNextLineSeparator = nextLine && nextLine.trim().startsWith('|') && nextLine.includes('---');
63
+ if (isTableStart && isNextLineSeparator) {
64
+ // Finish pending text block
65
+ if (currentBlockContent.length > 0) {
66
+ blocks.push({ type: 'text', content: currentBlockContent.join('\n') });
67
+ }
68
+ currentBlockContent = [line];
69
+ currentBlockType = 'table';
70
+ continue;
71
+ }
72
+ // 3. Handle Text
73
+ currentBlockContent.push(line);
74
+ }
75
+ // Push final block
76
+ if (currentBlockContent.length > 0) {
77
+ blocks.push({ type: currentBlockType, content: currentBlockContent.join('\n') });
78
+ }
79
+ return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, index) => {
80
+ if (block.type === 'table') {
81
+ if (!block.content.trim())
82
+ return null;
83
+ return _jsx(Table, { data: block.content }, index);
84
+ }
85
+ if (block.type === 'code') {
86
+ return (_jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: block.content }) }, index));
87
+ }
88
+ // Text Block
89
+ if (!block.content.trim())
90
+ return null;
91
+ return (_jsx(Box, { marginBottom: 0, children: _jsx(Text, { children: formatMessage(block.content) }) }, index));
92
+ }) }));
93
+ };
@@ -0,0 +1,275 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Table — Simple table renderer using basic ink components
4
+ *
5
+ * No external table library needed. Renders data as aligned columns.
6
+ */
7
+ import React, { useEffect, useState } from 'react';
8
+ import { Box, Text, useStdout } from 'ink';
9
+ const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
10
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
11
+ : null;
12
+ const COMBINING_MARK_PATTERN = /\p{Mark}/u;
13
+ const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\uFE0E\uFE0F]/u;
14
+ const DOUBLE_WIDTH_PATTERN = /[\u1100-\u115F\u2329\u232A\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}\u{1F1E6}-\u{1F1FF}]/u;
15
+ function splitGraphemes(text) {
16
+ if (!text)
17
+ return [];
18
+ if (graphemeSegmenter) {
19
+ return Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment);
20
+ }
21
+ return Array.from(text);
22
+ }
23
+ function getGraphemeWidth(grapheme) {
24
+ if (!grapheme)
25
+ return 0;
26
+ if (ZERO_WIDTH_PATTERN.test(grapheme))
27
+ return 0;
28
+ if (COMBINING_MARK_PATTERN.test(grapheme))
29
+ return 0;
30
+ if (/^[\u0000-\u001F\u007F-\u009F]$/.test(grapheme))
31
+ return 0;
32
+ if (DOUBLE_WIDTH_PATTERN.test(grapheme))
33
+ return 2;
34
+ return 1;
35
+ }
36
+ function getTextWidth(text) {
37
+ return splitGraphemes(text).reduce((width, grapheme) => width + getGraphemeWidth(grapheme), 0);
38
+ }
39
+ function takeByDisplayWidth(text, maxWidth) {
40
+ if (maxWidth <= 0) {
41
+ return { slice: '', remainder: text };
42
+ }
43
+ const graphemes = splitGraphemes(text);
44
+ let consumed = 0;
45
+ let width = 0;
46
+ while (consumed < graphemes.length) {
47
+ const nextWidth = getGraphemeWidth(graphemes[consumed]);
48
+ if (width + nextWidth > maxWidth)
49
+ break;
50
+ width += nextWidth;
51
+ consumed++;
52
+ }
53
+ return {
54
+ slice: graphemes.slice(0, consumed).join(''),
55
+ remainder: graphemes.slice(consumed).join(''),
56
+ };
57
+ }
58
+ function parseInlineMarkdown(text) {
59
+ const segments = [];
60
+ const pattern = /\*\*\*([\s\S]+?)\*\*\*|\*\*([\s\S]+?)\*\*|\*([\s\S]+?)\*/g;
61
+ let lastIndex = 0;
62
+ let match;
63
+ while ((match = pattern.exec(text)) !== null) {
64
+ if (match.index > lastIndex) {
65
+ segments.push({ text: text.slice(lastIndex, match.index) });
66
+ }
67
+ if (match[1] !== undefined) {
68
+ segments.push({ text: match[1], bold: true, italic: true });
69
+ }
70
+ else if (match[2] !== undefined) {
71
+ segments.push({ text: match[2], bold: true });
72
+ }
73
+ else if (match[3] !== undefined) {
74
+ segments.push({ text: match[3], italic: true });
75
+ }
76
+ lastIndex = pattern.lastIndex;
77
+ }
78
+ if (lastIndex < text.length) {
79
+ segments.push({ text: text.slice(lastIndex) });
80
+ }
81
+ return segments.length > 0 ? segments : [{ text }];
82
+ }
83
+ function getDisplayWidth(text) {
84
+ return text
85
+ .split('\n')
86
+ .reduce((maxWidth, line) => {
87
+ const lineWidth = parseInlineMarkdown(line)
88
+ .reduce((width, segment) => width + getTextWidth(segment.text), 0);
89
+ return Math.max(maxWidth, lineWidth);
90
+ }, 0);
91
+ }
92
+ function wrapStyledText(text, width) {
93
+ if (width <= 0) {
94
+ return [[{ text }]];
95
+ }
96
+ const lines = [];
97
+ const paragraphs = String(text).split('\n');
98
+ for (const paragraph of paragraphs) {
99
+ if (paragraph.trim() === '') {
100
+ lines.push([]);
101
+ continue;
102
+ }
103
+ const words = parseInlineMarkdown(paragraph).flatMap((segment) => segment.text
104
+ .split(/\s+/)
105
+ .filter(Boolean)
106
+ .map((word) => ({
107
+ text: word,
108
+ bold: segment.bold,
109
+ italic: segment.italic,
110
+ })));
111
+ let currentLine = [];
112
+ let currentLength = 0;
113
+ const pushCurrentLine = () => {
114
+ lines.push(currentLine);
115
+ currentLine = [];
116
+ currentLength = 0;
117
+ };
118
+ for (const word of words) {
119
+ let remainingWord = word.text;
120
+ while (remainingWord.length > 0) {
121
+ const availableWidth = currentLength === 0 ? width : width - currentLength - 1;
122
+ const remainingWidth = getTextWidth(remainingWord);
123
+ if (remainingWidth <= availableWidth) {
124
+ if (currentLength > 0) {
125
+ currentLine.push({ text: ' ' });
126
+ currentLength++;
127
+ }
128
+ currentLine.push({ ...word, text: remainingWord });
129
+ currentLength += remainingWidth;
130
+ remainingWord = '';
131
+ continue;
132
+ }
133
+ if (currentLength > 0) {
134
+ pushCurrentLine();
135
+ continue;
136
+ }
137
+ const { slice, remainder } = takeByDisplayWidth(remainingWord, width);
138
+ currentLine.push({ ...word, text: slice });
139
+ remainingWord = remainder;
140
+ pushCurrentLine();
141
+ }
142
+ }
143
+ if (currentLine.length > 0) {
144
+ pushCurrentLine();
145
+ }
146
+ }
147
+ return lines.length === 0 ? [[{ text: '' }]] : lines;
148
+ }
149
+ function getLineWidth(segments) {
150
+ return segments.reduce((width, segment) => width + getTextWidth(segment.text), 0);
151
+ }
152
+ /**
153
+ * Simple parser for Markdown tables.
154
+ */
155
+ function parseMarkdownTable(markdown) {
156
+ const lines = markdown.trim().split('\n');
157
+ if (lines.length < 3)
158
+ return null;
159
+ const hasPipes = lines[0].includes('|');
160
+ const hasSeparator = lines[1].includes('|') && lines[1].includes('-');
161
+ if (!hasPipes || !hasSeparator)
162
+ return null;
163
+ try {
164
+ const parseRow = (row) => row.split('|')
165
+ .map(cell => cell.trim())
166
+ .filter((cell, index, array) => {
167
+ if (index === 0 && cell === '')
168
+ return false;
169
+ if (index === array.length - 1 && cell === '')
170
+ return false;
171
+ return true;
172
+ });
173
+ const headers = parseRow(lines[0]);
174
+ const rows = lines.slice(2).map(parseRow);
175
+ return rows.map(row => {
176
+ const obj = {};
177
+ headers.forEach((header, i) => {
178
+ obj[header || `Column ${i + 1}`] = row[i] || '';
179
+ });
180
+ return obj;
181
+ });
182
+ }
183
+ catch (e) {
184
+ return null;
185
+ }
186
+ }
187
+ /**
188
+ * Normalize input data into an array of objects for display.
189
+ */
190
+ function normalizeData(data) {
191
+ let processedData = data;
192
+ if (typeof data === 'string') {
193
+ try {
194
+ const parsed = JSON.parse(data);
195
+ if (typeof parsed === 'object' && parsed !== null) {
196
+ processedData = parsed;
197
+ }
198
+ }
199
+ catch (e) {
200
+ const parsedMarkdown = parseMarkdownTable(data);
201
+ if (parsedMarkdown) {
202
+ processedData = parsedMarkdown;
203
+ }
204
+ }
205
+ }
206
+ if (Array.isArray(processedData)) {
207
+ return processedData.map(item => typeof item === 'object' && item !== null ? item : { value: String(item) });
208
+ }
209
+ else if (typeof processedData === 'object' && processedData !== null) {
210
+ return Object.entries(processedData).map(([key, value]) => ({
211
+ property: key,
212
+ value: typeof value === 'object' ? JSON.stringify(value) : String(value),
213
+ }));
214
+ }
215
+ return [{ value: String(processedData) }];
216
+ }
217
+ export const Table = ({ data, title, titleColor = 'cyan' }) => {
218
+ const { stdout } = useStdout();
219
+ const [terminalWidth, setTerminalWidth] = useState(stdout?.columns ?? 80);
220
+ useEffect(() => {
221
+ if (!stdout)
222
+ return;
223
+ const updateWidth = () => {
224
+ setTerminalWidth(stdout.columns ?? 80);
225
+ };
226
+ updateWidth();
227
+ stdout.on('resize', updateWidth);
228
+ return () => {
229
+ stdout.off('resize', updateWidth);
230
+ };
231
+ }, [stdout]);
232
+ const displayData = normalizeData(data);
233
+ if (displayData.length === 0) {
234
+ return (_jsx(Box, { marginY: 1, children: _jsx(Text, { italic: true, dimColor: true, children: "No data to display in table." }) }));
235
+ }
236
+ // Get all column keys
237
+ const columns = Array.from(new Set(displayData.flatMap(row => Object.keys(row))));
238
+ // Calculate column widths
239
+ const colWidths = columns.map(col => {
240
+ const headerLen = getDisplayWidth(String(col));
241
+ const maxCellLen = Math.max(...displayData.map(row => getDisplayWidth(String(row[col] ?? ''))));
242
+ return Math.max(headerLen, Math.min(maxCellLen, 100)) + 2;
243
+ });
244
+ // Adjust widths to fit terminal
245
+ let currentTotal = colWidths.reduce((a, b) => a + b, 0) + columns.length + 1;
246
+ const targetTotal = Math.max(40, terminalWidth - 2);
247
+ if (currentTotal > targetTotal) {
248
+ while (currentTotal > targetTotal) {
249
+ const widestIdx = colWidths.reduce((best, cur, idx) => cur > colWidths[best] ? idx : best, 0);
250
+ if (colWidths[widestIdx] <= 10)
251
+ break;
252
+ colWidths[widestIdx]--;
253
+ currentTotal--;
254
+ }
255
+ }
256
+ // Box-drawing lines
257
+ const topBorder = '┌' + colWidths.map(w => '─'.repeat(w)).join('┬') + '┐';
258
+ const headerSep = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤';
259
+ const rowSep = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤';
260
+ const bottomBorder = '└' + colWidths.map(w => '─'.repeat(w)).join('┴') + '┘';
261
+ const renderRowLines = (cells, rowKey, isHeader = false) => {
262
+ const wrappedCells = cells.map((cell, i) => wrapStyledText(cell, colWidths[i] - 2));
263
+ const maxLines = Math.max(...wrappedCells.map(c => c.length));
264
+ const lines = [];
265
+ for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
266
+ lines.push(_jsxs(Text, { bold: isHeader, children: ['│', wrappedCells.map((wrappedCell, i) => {
267
+ const segments = wrappedCell[lineIdx] ?? [];
268
+ const padding = Math.max(0, colWidths[i] - 1 - getLineWidth(segments));
269
+ return (_jsxs(React.Fragment, { children: [' ', segments.map((segment, segmentIdx) => (_jsx(Text, { bold: isHeader || segment.bold, italic: segment.italic, children: segment.text }, `${rowKey}-${lineIdx}-${i}-${segmentIdx}`))), ' '.repeat(padding), '│'] }, `${rowKey}-${lineIdx}-${i}`));
270
+ })] }, `${rowKey}-${lineIdx}`));
271
+ }
272
+ return lines;
273
+ };
274
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [title && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: titleColor, bold: true, underline: true, children: title }) })), _jsx(Text, { children: topBorder }), renderRowLines(columns.map(String), 'header', true), _jsx(Text, { children: headerSep }), displayData.map((row, rowIdx) => (_jsxs(React.Fragment, { children: [rowIdx > 0 && _jsx(Text, { dimColor: true, children: rowSep }), renderRowLines(columns.map(col => String(row[col] ?? '')), `row-${rowIdx}`)] }, rowIdx))), _jsx(Text, { children: bottomBorder })] }));
275
+ };