u-foo 2.5.13 → 2.5.15
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/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +339 -24
- package/src/code/commands.js +61 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +698 -0
- package/src/code/context/executionSegment.js +314 -0
- package/src/code/context/featureFlag.js +13 -0
- package/src/code/context/index.js +18 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +159 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +412 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +4 -1
- package/src/code/index.js +6 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +187 -31
- package/src/code/repl.js +36 -32
- package/src/code/sessionStore.js +227 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +65 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +32 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +260 -44
- package/src/ui/format/markdownRenderer.js +215 -72
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +408 -55
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { getArtifactsDir, loadArtifact, saveArtifact } = require("./artifacts");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_ARTIFACTS = 200;
|
|
8
|
+
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const DEFAULT_MAX_SESSION_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
/** Minimum gap between automatic GC passes during session saves. */
|
|
11
|
+
const DEFAULT_GC_MIN_INTERVAL_MS = 2 * 60 * 1000;
|
|
12
|
+
const GC_STAMP_NAME = ".gc-stamp";
|
|
13
|
+
|
|
14
|
+
function listArtifactFiles(workspaceRoot = process.cwd(), sessionId = "") {
|
|
15
|
+
const dir = getArtifactsDir(workspaceRoot, sessionId);
|
|
16
|
+
if (!fs.existsSync(dir)) return [];
|
|
17
|
+
try {
|
|
18
|
+
return fs.readdirSync(dir)
|
|
19
|
+
.filter((name) => name.endsWith(".json"))
|
|
20
|
+
.map((name) => {
|
|
21
|
+
const filePath = path.join(dir, name);
|
|
22
|
+
let stat = null;
|
|
23
|
+
try {
|
|
24
|
+
stat = fs.statSync(filePath);
|
|
25
|
+
} catch {
|
|
26
|
+
stat = null;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
artifactId: name.replace(/\.json$/, ""),
|
|
30
|
+
filePath,
|
|
31
|
+
sizeBytes: stat ? stat.size : 0,
|
|
32
|
+
mtimeMs: stat ? stat.mtimeMs : 0,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function markArtifactCold(workspaceRoot = process.cwd(), sessionId = "", artifactId = "") {
|
|
41
|
+
const loaded = loadArtifact(workspaceRoot, sessionId, artifactId);
|
|
42
|
+
if (!loaded.ok || !loaded.artifact) {
|
|
43
|
+
return { ok: false, error: loaded.error || "artifact not found", artifact: null };
|
|
44
|
+
}
|
|
45
|
+
const artifact = { ...loaded.artifact };
|
|
46
|
+
if (artifact.cold === true) {
|
|
47
|
+
return { ok: true, error: "", artifact, alreadyCold: true };
|
|
48
|
+
}
|
|
49
|
+
const preview = String(artifact.summary || "").slice(0, 600);
|
|
50
|
+
artifact.cold = true;
|
|
51
|
+
artifact.coldAt = new Date().toISOString();
|
|
52
|
+
artifact.raw = {
|
|
53
|
+
ok: true,
|
|
54
|
+
cold: true,
|
|
55
|
+
preview,
|
|
56
|
+
note: "raw content evicted; use transcript preview or re-run tool",
|
|
57
|
+
};
|
|
58
|
+
// Preserve existing index; avoid rebuild from cold stub
|
|
59
|
+
const saved = saveArtifact(workspaceRoot, sessionId, {
|
|
60
|
+
...artifact,
|
|
61
|
+
index: artifact.index || {},
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
ok: saved.ok,
|
|
65
|
+
error: saved.error || "",
|
|
66
|
+
artifact: saved.artifact,
|
|
67
|
+
alreadyCold: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveGcOptions(options = {}, env = process.env) {
|
|
72
|
+
const maxArtifacts = Number.isFinite(options.maxArtifacts)
|
|
73
|
+
? Math.max(1, Math.floor(options.maxArtifacts))
|
|
74
|
+
: (Number.parseInt(String(env.UFOO_UCODE_ARTIFACT_MAX_COUNT || ""), 10) || DEFAULT_MAX_ARTIFACTS);
|
|
75
|
+
const maxAgeMs = Number.isFinite(options.maxAgeMs)
|
|
76
|
+
? Math.max(1000, Math.floor(options.maxAgeMs))
|
|
77
|
+
: (Number.parseInt(String(env.UFOO_UCODE_ARTIFACT_MAX_AGE_MS || ""), 10) || DEFAULT_MAX_AGE_MS);
|
|
78
|
+
const maxSessionBytes = Number.isFinite(options.maxSessionBytes)
|
|
79
|
+
? Math.max(1024, Math.floor(options.maxSessionBytes))
|
|
80
|
+
: (Number.parseInt(String(env.UFOO_UCODE_ARTIFACT_MAX_BYTES || ""), 10) || DEFAULT_MAX_SESSION_BYTES);
|
|
81
|
+
const minIntervalMs = Number.isFinite(options.minIntervalMs)
|
|
82
|
+
? Math.max(0, Math.floor(options.minIntervalMs))
|
|
83
|
+
: (Number.parseInt(String(env.UFOO_UCODE_ARTIFACT_GC_INTERVAL_MS || ""), 10) || DEFAULT_GC_MIN_INTERVAL_MS);
|
|
84
|
+
return {
|
|
85
|
+
maxArtifacts,
|
|
86
|
+
maxAgeMs,
|
|
87
|
+
maxSessionBytes,
|
|
88
|
+
minIntervalMs,
|
|
89
|
+
nowMs: Number.isFinite(options.nowMs) ? options.nowMs : Date.now(),
|
|
90
|
+
dryRun: options.dryRun === true,
|
|
91
|
+
force: options.force === true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getGcStampPath(workspaceRoot = process.cwd(), sessionId = "") {
|
|
96
|
+
return path.join(getArtifactsDir(workspaceRoot, sessionId), GC_STAMP_NAME);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function countArtifactJsonFiles(workspaceRoot = process.cwd(), sessionId = "") {
|
|
100
|
+
const dir = getArtifactsDir(workspaceRoot, sessionId);
|
|
101
|
+
if (!fs.existsSync(dir)) return 0;
|
|
102
|
+
try {
|
|
103
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(".json")).length;
|
|
104
|
+
} catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function touchGcStamp(workspaceRoot = process.cwd(), sessionId = "", nowMs = Date.now()) {
|
|
110
|
+
const stampPath = getGcStampPath(workspaceRoot, sessionId);
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
|
|
113
|
+
fs.writeFileSync(stampPath, `${nowMs}\n`, "utf8");
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Throttled GC for session save / lifecycle hooks.
|
|
122
|
+
* Skips when a recent `.gc-stamp` exists, unless force=true or artifact
|
|
123
|
+
* count already exceeds maxArtifacts (cheap readdir pressure bypass).
|
|
124
|
+
*/
|
|
125
|
+
function maybeGcSessionArtifacts(workspaceRoot = process.cwd(), sessionId = "", options = {}) {
|
|
126
|
+
const id = String(sessionId || "").trim();
|
|
127
|
+
if (!id) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: "invalid session id",
|
|
131
|
+
skipped: true,
|
|
132
|
+
reason: "invalid_session",
|
|
133
|
+
scanned: 0,
|
|
134
|
+
actions: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const opts = resolveGcOptions(options);
|
|
139
|
+
const artifactsDir = getArtifactsDir(workspaceRoot, id);
|
|
140
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
error: "",
|
|
144
|
+
skipped: true,
|
|
145
|
+
reason: "no_artifacts_dir",
|
|
146
|
+
scanned: 0,
|
|
147
|
+
actions: [],
|
|
148
|
+
dryRun: opts.dryRun,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const stampPath = getGcStampPath(workspaceRoot, id);
|
|
153
|
+
|
|
154
|
+
if (!opts.force && opts.minIntervalMs > 0) {
|
|
155
|
+
let stampAgeOk = false;
|
|
156
|
+
try {
|
|
157
|
+
if (fs.existsSync(stampPath)) {
|
|
158
|
+
const st = fs.statSync(stampPath);
|
|
159
|
+
stampAgeOk = opts.nowMs - st.mtimeMs < opts.minIntervalMs;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
stampAgeOk = false;
|
|
163
|
+
}
|
|
164
|
+
if (stampAgeOk) {
|
|
165
|
+
const count = countArtifactJsonFiles(workspaceRoot, id);
|
|
166
|
+
if (count <= opts.maxArtifacts) {
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
error: "",
|
|
170
|
+
skipped: true,
|
|
171
|
+
reason: "throttled",
|
|
172
|
+
scanned: count,
|
|
173
|
+
actions: [],
|
|
174
|
+
dryRun: opts.dryRun,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = gcSessionArtifacts(workspaceRoot, id, options);
|
|
181
|
+
if (!opts.dryRun) touchGcStamp(workspaceRoot, id, opts.nowMs);
|
|
182
|
+
return {
|
|
183
|
+
...result,
|
|
184
|
+
skipped: false,
|
|
185
|
+
reason: opts.force ? "forced" : "interval",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function gcSessionArtifacts(workspaceRoot = process.cwd(), sessionId = "", options = {}) {
|
|
190
|
+
const opts = resolveGcOptions(options);
|
|
191
|
+
const files = listArtifactFiles(workspaceRoot, sessionId)
|
|
192
|
+
.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
193
|
+
const planned = [];
|
|
194
|
+
const deletedIds = new Set();
|
|
195
|
+
|
|
196
|
+
for (const item of files) {
|
|
197
|
+
if (opts.nowMs - item.mtimeMs > opts.maxAgeMs) {
|
|
198
|
+
planned.push({ action: "cold", artifactId: item.artifactId, reason: "age" });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let live = files.slice();
|
|
203
|
+
let totalBytes = live.reduce((sum, item) => sum + item.sizeBytes, 0);
|
|
204
|
+
while (live.length > opts.maxArtifacts || totalBytes > opts.maxSessionBytes) {
|
|
205
|
+
const oldest = live.shift();
|
|
206
|
+
if (!oldest) break;
|
|
207
|
+
if (deletedIds.has(oldest.artifactId)) continue;
|
|
208
|
+
planned.push({
|
|
209
|
+
action: "delete",
|
|
210
|
+
artifactId: oldest.artifactId,
|
|
211
|
+
reason: totalBytes > opts.maxSessionBytes ? "size" : "count",
|
|
212
|
+
});
|
|
213
|
+
deletedIds.add(oldest.artifactId);
|
|
214
|
+
totalBytes -= oldest.sizeBytes;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Prefer delete over cold for the same id
|
|
218
|
+
const byId = new Map();
|
|
219
|
+
for (const entry of planned) {
|
|
220
|
+
const prev = byId.get(entry.artifactId);
|
|
221
|
+
if (!prev || entry.action === "delete") byId.set(entry.artifactId, entry);
|
|
222
|
+
}
|
|
223
|
+
const actions = Array.from(byId.values());
|
|
224
|
+
|
|
225
|
+
const applied = [];
|
|
226
|
+
if (!opts.dryRun) {
|
|
227
|
+
for (const entry of actions) {
|
|
228
|
+
if (entry.action === "cold") {
|
|
229
|
+
const result = markArtifactCold(workspaceRoot, sessionId, entry.artifactId);
|
|
230
|
+
applied.push({ ...entry, ok: result.ok, error: result.error || "" });
|
|
231
|
+
} else if (entry.action === "delete") {
|
|
232
|
+
const filePath = path.join(getArtifactsDir(workspaceRoot, sessionId), `${entry.artifactId}.json`);
|
|
233
|
+
try {
|
|
234
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
235
|
+
applied.push({ ...entry, ok: true, error: "" });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
applied.push({
|
|
238
|
+
...entry,
|
|
239
|
+
ok: false,
|
|
240
|
+
error: err && err.message ? err.message : "delete failed",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
error: "",
|
|
250
|
+
scanned: files.length,
|
|
251
|
+
actions: opts.dryRun ? actions : applied,
|
|
252
|
+
dryRun: opts.dryRun,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function deleteSessionCommitLog(workspaceRoot = process.cwd(), sessionId = "") {
|
|
257
|
+
const id = String(sessionId || "").trim();
|
|
258
|
+
if (!id) return { ok: false, error: "invalid session id" };
|
|
259
|
+
const filePath = path.join(
|
|
260
|
+
path.resolve(workspaceRoot || process.cwd()),
|
|
261
|
+
".ufoo",
|
|
262
|
+
"agent",
|
|
263
|
+
"ucode",
|
|
264
|
+
"commits",
|
|
265
|
+
`${id}.jsonl`,
|
|
266
|
+
);
|
|
267
|
+
try {
|
|
268
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
269
|
+
return { ok: true, error: "", filePath };
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: err && err.message ? err.message : "failed to delete commit log",
|
|
274
|
+
filePath,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
DEFAULT_MAX_ARTIFACTS,
|
|
281
|
+
DEFAULT_MAX_AGE_MS,
|
|
282
|
+
DEFAULT_MAX_SESSION_BYTES,
|
|
283
|
+
DEFAULT_GC_MIN_INTERVAL_MS,
|
|
284
|
+
listArtifactFiles,
|
|
285
|
+
markArtifactCold,
|
|
286
|
+
gcSessionArtifacts,
|
|
287
|
+
maybeGcSessionArtifacts,
|
|
288
|
+
deleteSessionCommitLog,
|
|
289
|
+
resolveGcOptions,
|
|
290
|
+
getGcStampPath,
|
|
291
|
+
countArtifactJsonFiles,
|
|
292
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MAX_SYMBOLS = 80;
|
|
4
|
+
const MAX_REGIONS = 40;
|
|
5
|
+
|
|
6
|
+
function buildSymbolsFromContent(content = "", pathHint = "") {
|
|
7
|
+
const text = String(content || "");
|
|
8
|
+
const lines = text.split(/\r?\n/);
|
|
9
|
+
const symbols = [];
|
|
10
|
+
const patterns = [
|
|
11
|
+
{ kind: "function", re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)/ },
|
|
12
|
+
{ kind: "class", re: /^(?:export\s+)?class\s+([A-Za-z0-9_$]+)/ },
|
|
13
|
+
{ kind: "const", re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=/ },
|
|
14
|
+
{ kind: "method", re: /^\s*(?:async\s+)?([A-Za-z0-9_$]+)\s*\([^)]*\)\s*\{/ },
|
|
15
|
+
{ kind: "rust_fn", re: /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)/ },
|
|
16
|
+
{ kind: "python_def", re: /^(?:async\s+)?def\s+([A-Za-z0-9_]+)/ },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
for (const pattern of patterns) {
|
|
22
|
+
const match = line.match(pattern.re);
|
|
23
|
+
if (!match) continue;
|
|
24
|
+
const name = String(match[1] || "").trim();
|
|
25
|
+
if (!name || name === "if" || name === "for" || name === "while" || name === "switch") continue;
|
|
26
|
+
symbols.push({
|
|
27
|
+
name,
|
|
28
|
+
kind: pattern.kind,
|
|
29
|
+
line: i + 1,
|
|
30
|
+
path: pathHint || "",
|
|
31
|
+
});
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (symbols.length >= MAX_SYMBOLS) break;
|
|
35
|
+
}
|
|
36
|
+
return symbols;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildRegionsFromContent(content = "", pathHint = "") {
|
|
40
|
+
const text = String(content || "");
|
|
41
|
+
const lines = text.split(/\r?\n/);
|
|
42
|
+
const regions = [];
|
|
43
|
+
let current = null;
|
|
44
|
+
|
|
45
|
+
const startRe = /^(?:export\s+)?(?:async\s+)?(?:function|class)\s+[A-Za-z0-9_$]+|^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+[A-Za-z0-9_]+|^(?:async\s+)?def\s+[A-Za-z0-9_]+/;
|
|
46
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
if (startRe.test(line)) {
|
|
49
|
+
if (current) {
|
|
50
|
+
current.endLine = i;
|
|
51
|
+
regions.push(current);
|
|
52
|
+
}
|
|
53
|
+
current = {
|
|
54
|
+
path: pathHint || "",
|
|
55
|
+
startLine: i + 1,
|
|
56
|
+
endLine: i + 1,
|
|
57
|
+
label: line.trim().slice(0, 120),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (regions.length >= MAX_REGIONS) break;
|
|
61
|
+
}
|
|
62
|
+
if (current && regions.length < MAX_REGIONS) {
|
|
63
|
+
current.endLine = lines.length;
|
|
64
|
+
regions.push(current);
|
|
65
|
+
}
|
|
66
|
+
return regions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildArtifactIndex({
|
|
70
|
+
tool = "",
|
|
71
|
+
raw = null,
|
|
72
|
+
args = {},
|
|
73
|
+
} = {}) {
|
|
74
|
+
const name = String(tool || "").trim().toLowerCase();
|
|
75
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
76
|
+
const pathHint = String((args && args.path) || source.path || "").trim();
|
|
77
|
+
|
|
78
|
+
if (name === "read" || name === "skill") {
|
|
79
|
+
const content = String(source.content || "");
|
|
80
|
+
return {
|
|
81
|
+
symbols: buildSymbolsFromContent(content, pathHint),
|
|
82
|
+
regions: buildRegionsFromContent(content, pathHint),
|
|
83
|
+
path: pathHint,
|
|
84
|
+
totalLines: content ? content.split(/\r?\n/).length : 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (name === "bash") {
|
|
89
|
+
const command = String((args && args.command) || source.command || "");
|
|
90
|
+
const stdout = String(source.stdout || "");
|
|
91
|
+
if (/\bgit\s+(?:diff|show)\b/i.test(command)) {
|
|
92
|
+
const { parseGitDiffFiles } = require("./reducers");
|
|
93
|
+
return {
|
|
94
|
+
kind: "git_diff",
|
|
95
|
+
files: parseGitDiffFiles(stdout),
|
|
96
|
+
path: pathHint,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (/\b(rg|ripgrep|grep)\b/i.test(command)) {
|
|
100
|
+
const { parseSearchMatches } = require("./reducers");
|
|
101
|
+
const matches = parseSearchMatches(stdout);
|
|
102
|
+
return {
|
|
103
|
+
kind: "search",
|
|
104
|
+
matchCount: matches.length,
|
|
105
|
+
paths: Array.from(new Set(matches.map((m) => m.path))).slice(0, 40),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (/\b(npm test|jest|vitest|pytest|cargo test)\b/i.test(command)) {
|
|
109
|
+
const { extractTestFailures } = require("./reducers");
|
|
110
|
+
return {
|
|
111
|
+
kind: "test",
|
|
112
|
+
failures: extractTestFailures(stdout, String(source.stderr || "")),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findSymbolInIndex(index = {}, symbolName = "") {
|
|
121
|
+
const target = String(symbolName || "").trim().toLowerCase();
|
|
122
|
+
if (!target) return null;
|
|
123
|
+
const symbols = index && Array.isArray(index.symbols) ? index.symbols : [];
|
|
124
|
+
return symbols.find((entry) => String(entry.name || "").toLowerCase() === target) || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findRegionInIndex(index = {}, labelOrLine = "") {
|
|
128
|
+
const regions = index && Array.isArray(index.regions) ? index.regions : [];
|
|
129
|
+
if (regions.length === 0) return null;
|
|
130
|
+
const asLine = Number(labelOrLine);
|
|
131
|
+
if (Number.isFinite(asLine) && asLine > 0) {
|
|
132
|
+
return regions.find((region) => region.startLine <= asLine && region.endLine >= asLine) || null;
|
|
133
|
+
}
|
|
134
|
+
const label = String(labelOrLine || "").trim().toLowerCase();
|
|
135
|
+
if (!label) return null;
|
|
136
|
+
return regions.find((region) => String(region.label || "").toLowerCase().includes(label)) || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function selectorFromSymbol(index = {}, symbolName = "", padLines = 8) {
|
|
140
|
+
const symbol = findSymbolInIndex(index, symbolName);
|
|
141
|
+
if (!symbol) return null;
|
|
142
|
+
const startLine = Math.max(1, Number(symbol.line) - Math.max(0, padLines));
|
|
143
|
+
const endLine = Number(symbol.line) + Math.max(0, padLines);
|
|
144
|
+
return {
|
|
145
|
+
startLine,
|
|
146
|
+
endLine,
|
|
147
|
+
symbol: symbol.name,
|
|
148
|
+
kind: symbol.kind,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
MAX_SYMBOLS,
|
|
154
|
+
MAX_REGIONS,
|
|
155
|
+
buildSymbolsFromContent,
|
|
156
|
+
buildRegionsFromContent,
|
|
157
|
+
buildArtifactIndex,
|
|
158
|
+
findSymbolInIndex,
|
|
159
|
+
findRegionInIndex,
|
|
160
|
+
selectorFromSymbol,
|
|
161
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const { randomUUID } = require("crypto");
|
|
7
|
+
|
|
8
|
+
function getArtifactsDir(workspaceRoot = process.cwd(), sessionId = "") {
|
|
9
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
10
|
+
const id = String(sessionId || "").trim();
|
|
11
|
+
if (!id) return path.join(root, ".ufoo", "agent", "ucode", "artifacts");
|
|
12
|
+
return path.join(root, ".ufoo", "agent", "ucode", "artifacts", id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createArtifactId(prefix = "artifact") {
|
|
16
|
+
const safe = String(prefix || "artifact").trim().replace(/[^a-zA-Z0-9_-]+/g, "") || "artifact";
|
|
17
|
+
return `${safe}_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hashContent(value = "") {
|
|
21
|
+
return crypto.createHash("sha256").update(String(value || ""), "utf8").digest("hex").slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getArtifactFilePath(workspaceRoot = process.cwd(), sessionId = "", artifactId = "") {
|
|
25
|
+
const id = String(artifactId || "").trim();
|
|
26
|
+
if (!id) return "";
|
|
27
|
+
return path.join(getArtifactsDir(workspaceRoot, sessionId), `${id}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildArtifactRecord({
|
|
31
|
+
artifactId = "",
|
|
32
|
+
type = "tool_result",
|
|
33
|
+
source = "",
|
|
34
|
+
tool = "",
|
|
35
|
+
args = {},
|
|
36
|
+
raw = null,
|
|
37
|
+
summary = "",
|
|
38
|
+
index = {},
|
|
39
|
+
createdBy = "",
|
|
40
|
+
cold = false,
|
|
41
|
+
coldAt = "",
|
|
42
|
+
createdAt = "",
|
|
43
|
+
hash = "",
|
|
44
|
+
sizeBytes = null,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const rawText = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
47
|
+
const { buildArtifactIndex } = require("./artifactIndex");
|
|
48
|
+
const computedIndex = index && typeof index === "object" && Object.keys(index).length > 0
|
|
49
|
+
? index
|
|
50
|
+
: buildArtifactIndex({ tool, raw, args });
|
|
51
|
+
return {
|
|
52
|
+
artifactId: artifactId || createArtifactId(),
|
|
53
|
+
type: String(type || "tool_result"),
|
|
54
|
+
source: String(source || ""),
|
|
55
|
+
tool: String(tool || ""),
|
|
56
|
+
args: args && typeof args === "object" ? args : {},
|
|
57
|
+
hash: hash || hashContent(rawText),
|
|
58
|
+
sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : Buffer.byteLength(rawText, "utf8"),
|
|
59
|
+
createdAt: String(createdAt || "") || new Date().toISOString(),
|
|
60
|
+
createdBy: String(createdBy || tool || ""),
|
|
61
|
+
summary: String(summary || ""),
|
|
62
|
+
index: computedIndex && typeof computedIndex === "object" ? computedIndex : {},
|
|
63
|
+
cold: Boolean(cold),
|
|
64
|
+
coldAt: cold ? String(coldAt || new Date().toISOString()) : "",
|
|
65
|
+
raw,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveArtifact(workspaceRoot = process.cwd(), sessionId = "", record = {}) {
|
|
70
|
+
const payload = buildArtifactRecord(record);
|
|
71
|
+
const filePath = getArtifactFilePath(workspaceRoot, sessionId, payload.artifactId);
|
|
72
|
+
if (!filePath) {
|
|
73
|
+
return { ok: false, error: "invalid artifact id", artifact: null };
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
77
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
78
|
+
return { ok: true, error: "", artifact: payload, filePath };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: err && err.message ? err.message : "failed to save artifact",
|
|
83
|
+
artifact: payload,
|
|
84
|
+
filePath,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadArtifact(workspaceRoot = process.cwd(), sessionId = "", artifactId = "") {
|
|
90
|
+
const filePath = getArtifactFilePath(workspaceRoot, sessionId, artifactId);
|
|
91
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
92
|
+
return { ok: false, error: `artifact not found: ${artifactId}`, artifact: null, filePath };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
96
|
+
return { ok: true, error: "", artifact: parsed, filePath };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: err && err.message ? err.message : "failed to load artifact",
|
|
101
|
+
artifact: null,
|
|
102
|
+
filePath,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readArtifactSlice(artifact = null, selector = {}) {
|
|
108
|
+
const record = artifact && typeof artifact === "object" ? artifact : null;
|
|
109
|
+
if (!record) return { ok: false, error: "missing artifact", content: "" };
|
|
110
|
+
const raw = record.raw;
|
|
111
|
+
const sel = selector && typeof selector === "object" ? selector : {};
|
|
112
|
+
|
|
113
|
+
if (record.type === "source_file" || record.tool === "read") {
|
|
114
|
+
const content = raw && typeof raw === "object" ? String(raw.content || "") : String(raw || "");
|
|
115
|
+
const startLine = Number(sel.startLine || sel.start || 0);
|
|
116
|
+
const endLine = Number(sel.endLine || sel.end || 0);
|
|
117
|
+
if (startLine > 0 && endLine >= startLine) {
|
|
118
|
+
const lines = content.split(/\r?\n/);
|
|
119
|
+
const slice = lines.slice(startLine - 1, endLine).join("\n");
|
|
120
|
+
return { ok: true, error: "", content: slice, range: `${startLine}-${endLine}` };
|
|
121
|
+
}
|
|
122
|
+
const maxChars = Number(sel.maxChars || 8000);
|
|
123
|
+
if (content.length > maxChars) {
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
error: "",
|
|
127
|
+
content: `${content.slice(0, maxChars)}\n...[truncated]`,
|
|
128
|
+
truncated: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, error: "", content };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (record.tool === "bash") {
|
|
135
|
+
const stdout = raw && typeof raw === "object" ? String(raw.stdout || "") : "";
|
|
136
|
+
const stderr = raw && typeof raw === "object" ? String(raw.stderr || "") : "";
|
|
137
|
+
const tail = Number(sel.tailLines || 40);
|
|
138
|
+
const tailStdout = stdout.split(/\r?\n/).slice(-tail).join("\n");
|
|
139
|
+
const tailStderr = stderr.split(/\r?\n/).slice(-tail).join("\n");
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
error: "",
|
|
143
|
+
content: JSON.stringify({
|
|
144
|
+
exitCode: raw && typeof raw === "object" ? raw.code : null,
|
|
145
|
+
stdout: tailStdout,
|
|
146
|
+
stderr: tailStderr,
|
|
147
|
+
}, null, 2),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const text = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
|
|
152
|
+
const maxChars = Number(sel.maxChars || 12000);
|
|
153
|
+
if (text.length > maxChars) {
|
|
154
|
+
return { ok: true, error: "", content: `${text.slice(0, maxChars)}\n...[truncated]`, truncated: true };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, error: "", content: text };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function deleteSessionArtifacts(workspaceRoot = process.cwd(), sessionId = "") {
|
|
160
|
+
const dir = getArtifactsDir(workspaceRoot, sessionId);
|
|
161
|
+
if (!fs.existsSync(dir)) return { ok: true, error: "" };
|
|
162
|
+
try {
|
|
163
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
164
|
+
return { ok: true, error: "" };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: err && err.message ? err.message : "failed to delete artifacts",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
getArtifactsDir,
|
|
175
|
+
createArtifactId,
|
|
176
|
+
hashContent,
|
|
177
|
+
getArtifactFilePath,
|
|
178
|
+
buildArtifactRecord,
|
|
179
|
+
saveArtifact,
|
|
180
|
+
loadArtifact,
|
|
181
|
+
readArtifactSlice,
|
|
182
|
+
deleteSessionArtifacts,
|
|
183
|
+
};
|