protoagent 0.1.3 → 0.1.5
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 +34 -3
- package/dist/App.js +134 -81
- package/dist/agentic-loop.js +31 -12
- package/dist/cli.js +59 -2
- package/dist/components/CollapsibleBox.js +2 -2
- package/dist/components/ConsolidatedToolMessage.js +3 -11
- package/dist/components/FormattedMessage.js +80 -4
- package/dist/config.js +199 -71
- package/dist/runtime-config.js +29 -14
- package/dist/sub-agent.js +4 -1
- package/dist/system-prompt.js +3 -1
- package/dist/tools/bash.js +23 -3
- package/dist/tools/edit-file.js +248 -16
- package/dist/tools/index.js +2 -2
- package/dist/tools/read-file.js +89 -3
- package/dist/tools/search-files.js +92 -1
- package/dist/utils/compactor.js +2 -1
- package/dist/utils/file-time.js +54 -0
- package/package.json +1 -1
- package/dist/components/Table.js +0 -275
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File read-time tracking — staleness guard for edit_file.
|
|
3
|
+
*
|
|
4
|
+
* Ensures the model has read a file before editing it,
|
|
5
|
+
* and that the file hasn't changed on disk since it was last read.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
const readTimes = new Map(); // key: "sessionId:absolutePath" → epoch ms
|
|
9
|
+
/**
|
|
10
|
+
* Record that a file was read at the current time.
|
|
11
|
+
*/
|
|
12
|
+
export function recordRead(sessionId, absolutePath) {
|
|
13
|
+
readTimes.set(`${sessionId}:${absolutePath}`, Date.now());
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Assert that a file was previously read and hasn't changed on disk since.
|
|
17
|
+
* Throws if the file was never read or has been modified.
|
|
18
|
+
*/
|
|
19
|
+
export function assertReadBefore(sessionId, absolutePath) {
|
|
20
|
+
const key = `${sessionId}:${absolutePath}`;
|
|
21
|
+
const lastRead = readTimes.get(key);
|
|
22
|
+
if (!lastRead) {
|
|
23
|
+
throw new Error(`You must read '${absolutePath}' before editing it. Call read_file first.`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const mtime = fs.statSync(absolutePath).mtimeMs;
|
|
27
|
+
if (mtime > lastRead + 100) {
|
|
28
|
+
// Clear stale entry so the error message stays accurate
|
|
29
|
+
readTimes.delete(key);
|
|
30
|
+
throw new Error(`'${absolutePath}' has changed on disk since you last read it. Re-read it before editing.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err.code === 'ENOENT') {
|
|
35
|
+
readTimes.delete(key);
|
|
36
|
+
throw new Error(`'${absolutePath}' no longer exists on disk.`);
|
|
37
|
+
}
|
|
38
|
+
// Re-throw our own errors
|
|
39
|
+
if (err.message.includes('has changed on disk') || err.message.includes('must read')) {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
// Ignore other stat errors — don't block edits on stat failures
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clear all read-time entries for a session (e.g. on session end).
|
|
47
|
+
*/
|
|
48
|
+
export function clearSession(sessionId) {
|
|
49
|
+
for (const key of readTimes.keys()) {
|
|
50
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
51
|
+
readTimes.delete(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
package/dist/components/Table.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
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
|
-
};
|