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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +187 -31
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+
3
+ const { loadArtifact, readArtifactSlice } = require("./artifacts");
4
+
5
+ function emptyWorkingSet() {
6
+ return [];
7
+ }
8
+
9
+ function normalizeWorkingSetEntry(entry = {}) {
10
+ const source = entry && typeof entry === "object" ? entry : {};
11
+ return {
12
+ artifactId: String(source.artifactId || "").trim(),
13
+ selector: source.selector && typeof source.selector === "object" ? source.selector : {},
14
+ intent: String(source.intent || "inspect").trim(),
15
+ retention: String(source.retention || "retain_region").trim(),
16
+ expiresWhen: String(source.expiresWhen || "").trim(),
17
+ priority: Number.isFinite(source.priority) ? source.priority : 0.5,
18
+ addedAt: source.addedAt || new Date().toISOString(),
19
+ };
20
+ }
21
+
22
+ function workingSetArtifactIds(workingSet = []) {
23
+ return new Set(
24
+ (Array.isArray(workingSet) ? workingSet : [])
25
+ .map((entry) => String(entry && entry.artifactId || "").trim())
26
+ .filter(Boolean),
27
+ );
28
+ }
29
+
30
+ function isWorkingSetEntryExpired(entry = {}, session = {}) {
31
+ const expires = String(entry.expiresWhen || "").trim();
32
+ if (!expires) return false;
33
+ const turnMatch = expires.match(/^turn:\+(\d+)$/i);
34
+ if (turnMatch) {
35
+ const addedTurn = Number(entry.addedAtTurn);
36
+ const currentTurn = Number(session.currentTurn);
37
+ if (!Number.isFinite(addedTurn) || !Number.isFinite(currentTurn)) return false;
38
+ return currentTurn - addedTurn > Number.parseInt(turnMatch[1], 10);
39
+ }
40
+ if (/^\d{4}-\d{2}-\d{2}/.test(expires)) {
41
+ const deadline = new Date(expires);
42
+ if (!Number.isNaN(deadline.getTime())) return Date.now() > deadline.getTime();
43
+ }
44
+ return false;
45
+ }
46
+
47
+ function pruneExpiredWorkingSetEntries(workingSet = [], session = {}) {
48
+ const list = Array.isArray(workingSet) ? workingSet.map(normalizeWorkingSetEntry) : [];
49
+ const expired = [];
50
+ const kept = list.filter((entry) => {
51
+ if (!isWorkingSetEntryExpired(entry, session)) return true;
52
+ expired.push(entry.artifactId);
53
+ return false;
54
+ });
55
+ for (const artifactId of expired) {
56
+ recordVeto(session, "ttl_expired", artifactId, "");
57
+ }
58
+ return kept;
59
+ }
60
+
61
+ const MAX_WORKING_SET = 12;
62
+ const MAX_REHYDRATE_PER_PLAN = 4;
63
+
64
+ function recordVeto(session = {}, type = "", artifactId = "", detail = "") {
65
+ if (!session || typeof session !== "object") return;
66
+ session.lastContextVetoes = [
67
+ ...(Array.isArray(session.lastContextVetoes) ? session.lastContextVetoes : []),
68
+ {
69
+ at: new Date().toISOString(),
70
+ type: String(type || "veto"),
71
+ artifactId: String(artifactId || ""),
72
+ detail: String(detail || ""),
73
+ },
74
+ ].slice(-20);
75
+ }
76
+
77
+ function applyRehydrateNext(entries = [], plan = {}, session = {}) {
78
+ const list = Array.isArray(entries) ? entries.slice() : [];
79
+ const byId = new Set(list.map((e) => e.artifactId).filter(Boolean));
80
+ const workspaceRoot = session.workspaceRoot || process.cwd();
81
+ const sessionId = session.sessionId || "";
82
+ const requested = Array.isArray(plan.rehydrateNext) ? plan.rehydrateNext : [];
83
+ let accepted = 0;
84
+ let processed = 0;
85
+
86
+ for (const item of requested) {
87
+ const id = typeof item === "string"
88
+ ? String(item || "").trim()
89
+ : String((item && item.artifactId) || "").trim();
90
+ if (!id) continue;
91
+ if (processed >= MAX_REHYDRATE_PER_PLAN) {
92
+ recordVeto(session, "rehydrate_cap", id, `max ${MAX_REHYDRATE_PER_PLAN} per plan`);
93
+ break;
94
+ }
95
+ processed += 1;
96
+ if (byId.has(id)) continue;
97
+
98
+ const loaded = loadArtifact(workspaceRoot, sessionId, id);
99
+ if (!loaded.ok || !loaded.artifact) {
100
+ recordVeto(session, "rehydrate_missing", id, loaded.error || "not found");
101
+ continue;
102
+ }
103
+ if (loaded.artifact.cold === true) {
104
+ // Cold artifacts may rehydrate as preview-only regions, never retain_raw.
105
+ const entry = normalizeWorkingSetEntry({
106
+ artifactId: id,
107
+ intent: "rehydrate_cold",
108
+ retention: "retain_region",
109
+ priority: 0.55,
110
+ selector: (item && typeof item === "object" && item.selector) || { maxChars: 800 },
111
+ });
112
+ if (list.length >= MAX_WORKING_SET) {
113
+ const lowestIdx = list.reduce((best, cur, idx) => (
114
+ cur.priority < list[best].priority ? idx : best
115
+ ), 0);
116
+ if (list[lowestIdx].priority >= entry.priority) {
117
+ recordVeto(session, "rehydrate_rejected", id, "working set full");
118
+ continue;
119
+ }
120
+ recordVeto(session, "cap_evicted", list[lowestIdx].artifactId, "displaced by rehydrate");
121
+ byId.delete(list[lowestIdx].artifactId);
122
+ list.splice(lowestIdx, 1);
123
+ }
124
+ list.push(entry);
125
+ byId.add(id);
126
+ accepted += 1;
127
+ continue;
128
+ }
129
+
130
+ const entry = normalizeWorkingSetEntry({
131
+ artifactId: id,
132
+ intent: "rehydrate",
133
+ retention: "retain_region",
134
+ priority: 0.7,
135
+ selector: (item && typeof item === "object" && item.selector) || {},
136
+ });
137
+ if (list.length >= MAX_WORKING_SET) {
138
+ const lowestIdx = list.reduce((best, cur, idx) => (
139
+ cur.priority < list[best].priority ? idx : best
140
+ ), 0);
141
+ if (list[lowestIdx].priority >= entry.priority) {
142
+ recordVeto(session, "rehydrate_rejected", id, "working set full");
143
+ continue;
144
+ }
145
+ recordVeto(session, "cap_evicted", list[lowestIdx].artifactId, "displaced by rehydrate");
146
+ byId.delete(list[lowestIdx].artifactId);
147
+ list.splice(lowestIdx, 1);
148
+ }
149
+ list.push(entry);
150
+ byId.add(id);
151
+ accepted += 1;
152
+ }
153
+
154
+ return list.slice(0, MAX_WORKING_SET);
155
+ }
156
+
157
+ function applyWorkingSetPlan(workingSet = [], plan = {}, session = {}) {
158
+ const current = pruneExpiredWorkingSetEntries(
159
+ Array.isArray(workingSet) ? workingSet.map(normalizeWorkingSetEntry) : [],
160
+ session,
161
+ );
162
+ const source = plan && typeof plan === "object" ? plan : {};
163
+ const byId = new Map(current.map((e) => [e.artifactId, e]));
164
+
165
+ for (const artifactId of source.retainRaw || []) {
166
+ const id = String(artifactId || "").trim();
167
+ if (!id) continue;
168
+ byId.set(id, normalizeWorkingSetEntry({
169
+ artifactId: id,
170
+ intent: "retain_raw",
171
+ retention: "retain_raw",
172
+ priority: 0.95,
173
+ ...(byId.get(id) || {}),
174
+ }));
175
+ }
176
+
177
+ for (const entry of source.retainRegions || []) {
178
+ if (!entry || typeof entry !== "object") continue;
179
+ const id = String(entry.artifactId || "").trim();
180
+ if (!id) continue;
181
+ byId.set(id, normalizeWorkingSetEntry({
182
+ artifactId: id,
183
+ selector: entry.selector || {},
184
+ intent: entry.intent || "inspect",
185
+ retention: "retain_region",
186
+ priority: entry.priority || 0.8,
187
+ ...(byId.get(id) || {}),
188
+ }));
189
+ }
190
+
191
+ for (const artifactId of source.evict || []) {
192
+ byId.delete(String(artifactId || "").trim());
193
+ }
194
+
195
+ for (const entry of source.summarize || []) {
196
+ if (!entry || typeof entry !== "object") continue;
197
+ const id = String(entry.artifactId || "").trim();
198
+ if (!id) continue;
199
+ const existing = byId.get(id) || normalizeWorkingSetEntry({ artifactId: id });
200
+ byId.set(id, normalizeWorkingSetEntry({
201
+ ...existing,
202
+ intent: "summarized",
203
+ retention: "retain_region",
204
+ priority: Math.min(existing.priority || 0.5, 0.45),
205
+ selector: entry.selector && typeof entry.selector === "object" ? entry.selector : { maxChars: 1200 },
206
+ }));
207
+ }
208
+
209
+ const sorted = Array.from(byId.values()).sort((a, b) => b.priority - a.priority);
210
+ const evicted = sorted.slice(MAX_WORKING_SET).map((entry) => entry.artifactId).filter(Boolean);
211
+ let next = sorted.slice(0, MAX_WORKING_SET);
212
+ for (const artifactId of evicted) {
213
+ recordVeto(session, "cap_evicted", artifactId, "working set cap");
214
+ }
215
+
216
+ next = applyRehydrateNext(next, source, session);
217
+ return next.slice(0, MAX_WORKING_SET);
218
+ }
219
+
220
+ function hydrateWorkingSetEntry(entry = {}, session = {}) {
221
+ const normalized = normalizeWorkingSetEntry(entry);
222
+ if (!normalized.artifactId) return null;
223
+ const loaded = loadArtifact(
224
+ session.workspaceRoot || process.cwd(),
225
+ session.sessionId || "",
226
+ normalized.artifactId,
227
+ );
228
+ if (!loaded.ok || !loaded.artifact) return null;
229
+ let selector = normalized.selector && typeof normalized.selector === "object"
230
+ ? { ...normalized.selector }
231
+ : {};
232
+ if (selector.symbol && (!selector.startLine || !selector.endLine)) {
233
+ const { selectorFromSymbol } = require("./artifactIndex");
234
+ const fromSymbol = selectorFromSymbol(loaded.artifact.index || {}, selector.symbol);
235
+ if (fromSymbol) {
236
+ selector = { ...selector, ...fromSymbol };
237
+ }
238
+ }
239
+ const slice = readArtifactSlice(loaded.artifact, selector);
240
+ return {
241
+ ...normalized,
242
+ selector,
243
+ preview: slice.content,
244
+ truncated: Boolean(slice.truncated),
245
+ range: slice.range,
246
+ };
247
+ }
248
+
249
+ function renderWorkingSetContext(workingSet = [], session = {}) {
250
+ const list = Array.isArray(workingSet) ? workingSet : [];
251
+ if (list.length === 0) return "";
252
+ const lines = ["Current Working Set:"];
253
+ for (const entry of list) {
254
+ const hydrated = hydrateWorkingSetEntry(entry, session);
255
+ if (!hydrated) {
256
+ lines.push(`- artifact://${entry.artifactId} (${entry.intent}) [missing]`);
257
+ continue;
258
+ }
259
+ lines.push(`- artifact://${hydrated.artifactId} (${hydrated.intent}, priority=${hydrated.priority})`);
260
+ if (hydrated.preview) {
261
+ lines.push(` preview:\n${String(hydrated.preview).split(/\r?\n/).map((l) => ` ${l}`).join("\n")}`);
262
+ }
263
+ }
264
+ return lines.join("\n");
265
+ }
266
+
267
+ function pruneWorkingSetByRetention(workingSet = [], session = {}) {
268
+ const list = pruneExpiredWorkingSetEntries(
269
+ Array.isArray(workingSet) ? workingSet.map(normalizeWorkingSetEntry) : [],
270
+ session,
271
+ );
272
+ const segmentId = String(session.executionState && session.executionState.currentSegmentId || "").trim();
273
+ return list.filter((entry) => {
274
+ if (entry.expiresWhen === "patch_applied" && entry.intent === "modify") {
275
+ const modified = session.executionState && Array.isArray(session.executionState.modifiedFiles)
276
+ ? session.executionState.modifiedFiles
277
+ : [];
278
+ const pathHint = entry.selector && entry.selector.path ? String(entry.selector.path) : "";
279
+ if (pathHint && modified.includes(pathHint)) return false;
280
+ }
281
+ if (entry.expiresWhen === "segment_end" && segmentId && entry.addedAt) {
282
+ // Keep during active segment only when priority is high
283
+ return entry.priority >= 0.7;
284
+ }
285
+ return true;
286
+ });
287
+ }
288
+
289
+ function defaultContextPlanFromToolEvent(tool = "", artifactId = "", args = {}) {
290
+ if (!artifactId) return null;
291
+ const name = String(tool || "").trim().toLowerCase();
292
+ if (name === "read" || name === "bash") {
293
+ const selector = {};
294
+ if (args && args.path) selector.path = String(args.path);
295
+ return {
296
+ retainRegions: [{ artifactId, intent: "inspect", priority: 0.75, selector }],
297
+ };
298
+ }
299
+ if (name === "write" || name === "edit") {
300
+ const path = args && args.path ? String(args.path) : "";
301
+ return {
302
+ retainRaw: [artifactId],
303
+ retainRegions: path ? [{ artifactId, intent: "modify", priority: 0.85, selector: { path } }] : [],
304
+ };
305
+ }
306
+ return { retainRaw: [artifactId] };
307
+ }
308
+
309
+ module.exports = {
310
+ MAX_WORKING_SET,
311
+ MAX_REHYDRATE_PER_PLAN,
312
+ emptyWorkingSet,
313
+ normalizeWorkingSetEntry,
314
+ applyWorkingSetPlan,
315
+ applyRehydrateNext,
316
+ hydrateWorkingSetEntry,
317
+ renderWorkingSetContext,
318
+ workingSetArtifactIds,
319
+ pruneWorkingSetByRetention,
320
+ pruneExpiredWorkingSetEntries,
321
+ isWorkingSetEntryExpired,
322
+ defaultContextPlanFromToolEvent,
323
+ };
@@ -2,8 +2,9 @@ const { runReadTool } = require("./tools/read");
2
2
  const { runWriteTool } = require("./tools/write");
3
3
  const { runEditTool } = require("./tools/edit");
4
4
  const { runBashTool } = require("./tools/bash");
5
+ const { runArtifactReadTool } = require("./tools/artifactRead");
5
6
 
6
- const TOOL_NAMES = ["read", "write", "edit", "bash"];
7
+ const TOOL_NAMES = ["read", "write", "edit", "bash", "artifact_read"];
7
8
 
8
9
  function normalizeToolName(value = "") {
9
10
  const text = String(value || "").trim().toLowerCase();
@@ -11,6 +12,7 @@ function normalizeToolName(value = "") {
11
12
  if (text === "write") return "write";
12
13
  if (text === "edit") return "edit";
13
14
  if (text === "bash") return "bash";
15
+ if (text === "artifact_read" || text === "artifact-read" || text === "artifactread") return "artifact_read";
14
16
  return "";
15
17
  }
16
18
 
@@ -27,6 +29,7 @@ function runToolCall(input = {}, options = {}) {
27
29
  if (tool === "read") return runReadTool(args, options);
28
30
  if (tool === "write") return runWriteTool(args, options);
29
31
  if (tool === "edit") return runEditTool(args, options);
32
+ if (tool === "artifact_read") return runArtifactReadTool(args, options);
30
33
  return runBashTool(args, options);
31
34
  }
32
35
 
package/src/code/index.js CHANGED
@@ -31,7 +31,10 @@ const {
31
31
  getSessionFilePath,
32
32
  saveSessionSnapshot,
33
33
  loadSessionSnapshot,
34
+ listSessionSummaries,
35
+ deleteSessionData,
34
36
  } = require("./sessionStore");
37
+ const context = require("./context");
35
38
  const launcher = require("./launcher");
36
39
 
37
40
  module.exports = {
@@ -68,5 +71,8 @@ module.exports = {
68
71
  getSessionFilePath,
69
72
  saveSessionSnapshot,
70
73
  loadSessionSnapshot,
74
+ listSessionSummaries,
75
+ deleteSessionData,
76
+ context,
71
77
  ...launcher,
72
78
  };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const { saveGlobalUcodeConfig } = require("../config");
4
+
5
+ /**
6
+ * Apply /model show|set against the live session state.
7
+ * Persists ucodeModel to the global config so the next launch keeps it.
8
+ */
9
+ function applyUcodeModelCommand(state = {}, result = {}) {
10
+ const action = String((result && result.action) || "").trim().toLowerCase();
11
+ if (action === "show") {
12
+ const model = String((state && state.model) || "").trim() || "(unset)";
13
+ const provider = String((state && state.provider) || "").trim() || "(unset)";
14
+ return {
15
+ ok: true,
16
+ error: "",
17
+ output: [
18
+ `model: ${model}`,
19
+ `provider: ${provider}`,
20
+ "usage: /model <model-id>",
21
+ ].join("\n"),
22
+ model: String((state && state.model) || "").trim(),
23
+ };
24
+ }
25
+ if (action === "set") {
26
+ const next = String((result && result.model) || "").trim();
27
+ if (!next) {
28
+ return {
29
+ ok: false,
30
+ error: "usage: /model [model-id]",
31
+ output: "usage: /model [model-id]",
32
+ };
33
+ }
34
+ const previous = String((state && state.model) || "").trim();
35
+ if (state && typeof state === "object") state.model = next;
36
+ try {
37
+ saveGlobalUcodeConfig({ ucodeModel: next });
38
+ } catch {
39
+ // best-effort persistence
40
+ }
41
+ try {
42
+ process.env.UFOO_UCODE_MODEL = next;
43
+ } catch {
44
+ // ignore env write failures
45
+ }
46
+ const output = previous && previous !== next
47
+ ? `model switched: ${previous} → ${next}`
48
+ : `model set: ${next}`;
49
+ return {
50
+ ok: true,
51
+ error: "",
52
+ output,
53
+ model: next,
54
+ previous,
55
+ };
56
+ }
57
+ return {
58
+ ok: false,
59
+ error: "usage: /model [model-id]",
60
+ output: "usage: /model [model-id]",
61
+ };
62
+ }
63
+
64
+ function suggestUcodeModels(state = {}) {
65
+ const current = String((state && state.model) || "").trim();
66
+ const provider = String((state && state.provider) || "").trim().toLowerCase();
67
+ let defaults = ["gpt-5.4", "gpt-5.3", "o3", "o4-mini"];
68
+ if (provider.includes("anthropic") || provider.includes("claude")) {
69
+ defaults = ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"];
70
+ } else if (provider.includes("kimi") || provider.includes("moonshot")) {
71
+ defaults = ["kimi-k2.5", "moonshot-v1-128k"];
72
+ }
73
+ const ids = [];
74
+ if (current) ids.push(current);
75
+ for (const id of defaults) {
76
+ if (id && !ids.includes(id)) ids.push(id);
77
+ }
78
+ return ids.map((id) => ({
79
+ id,
80
+ desc: id === current ? "current" : "",
81
+ }));
82
+ }
83
+
84
+ module.exports = {
85
+ applyUcodeModelCommand,
86
+ suggestUcodeModels,
87
+ };