@pi-unipi/utility 0.2.5 → 0.2.7
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 +54 -1
- package/package.json +7 -2
- package/src/commands.ts +41 -3
- package/src/diff/highlighter.ts +353 -0
- package/src/diff/parser.ts +191 -0
- package/src/diff/renderer.ts +422 -0
- package/src/diff/settings.ts +199 -0
- package/src/diff/theme.ts +319 -0
- package/src/diff/wrapper.ts +287 -0
- package/src/index.ts +68 -8
- package/src/tui/badge-settings.ts +17 -58
- package/src/tui/util-settings-tui.ts +498 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Diff Tool Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps the default Pi write/edit tools with Shiki-powered diff rendering.
|
|
5
|
+
* When enabled, enhanced tools are registered that:
|
|
6
|
+
* 1. Read old content before write
|
|
7
|
+
* 2. Delegate to the original tool
|
|
8
|
+
* 3. Compute diff
|
|
9
|
+
* 4. Store diff in result.details for async rendering
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import type { ExtensionAPI, ToolDefinition, AgentToolResult } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { readDiffSettings } from "./settings.js";
|
|
16
|
+
import { parseDiff } from "./parser.js";
|
|
17
|
+
import { resolveDiffColors, applyDiffPalette } from "./theme.js";
|
|
18
|
+
import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH } from "./renderer.js";
|
|
19
|
+
import { detectLanguageFromPath, hlBlock, MAX_HL_CHARS } from "./highlighter.js";
|
|
20
|
+
|
|
21
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Extended tool result with diff data */
|
|
24
|
+
interface DiffToolDetails {
|
|
25
|
+
/** Old file content (null for new files) */
|
|
26
|
+
oldContent: string | null;
|
|
27
|
+
/** New file content */
|
|
28
|
+
newContent: string;
|
|
29
|
+
/** Parsed diff */
|
|
30
|
+
diff: ReturnType<typeof parseDiff>;
|
|
31
|
+
/** File path */
|
|
32
|
+
filePath: string;
|
|
33
|
+
/** Detected language */
|
|
34
|
+
language: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Edit operation from the edit tool */
|
|
38
|
+
export interface EditOperation {
|
|
39
|
+
oldText: string;
|
|
40
|
+
newText: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize edit tool input to get the list of edit operations.
|
|
47
|
+
* Handles both single-edit and multi-edit parameter formats.
|
|
48
|
+
*/
|
|
49
|
+
export function getEditOperations(input: any): EditOperation[] {
|
|
50
|
+
if (Array.isArray(input?.edits)) {
|
|
51
|
+
return input.edits.map((e: any) => ({
|
|
52
|
+
oldText: e.oldText ?? e.old_text ?? "",
|
|
53
|
+
newText: e.newText ?? e.new_text ?? "",
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
if (input?.oldText !== undefined || input?.old_text !== undefined) {
|
|
57
|
+
return [{
|
|
58
|
+
oldText: input.oldText ?? input.old_text ?? "",
|
|
59
|
+
newText: input.newText ?? input.new_text ?? "",
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Summarize edit operations into aggregate diff stats.
|
|
67
|
+
*/
|
|
68
|
+
export function summarizeEditOperations(operations: EditOperation[]): {
|
|
69
|
+
totalEdits: number;
|
|
70
|
+
totalAdditions: number;
|
|
71
|
+
totalDeletions: number;
|
|
72
|
+
} {
|
|
73
|
+
let totalAdditions = 0;
|
|
74
|
+
let totalDeletions = 0;
|
|
75
|
+
|
|
76
|
+
for (const op of operations) {
|
|
77
|
+
const oldLines = op.oldText.split("\n");
|
|
78
|
+
const newLines = op.newText.split("\n");
|
|
79
|
+
totalDeletions += oldLines.length;
|
|
80
|
+
totalAdditions += newLines.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
totalEdits: operations.length,
|
|
85
|
+
totalAdditions,
|
|
86
|
+
totalDeletions,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read file content safely. Returns null if file doesn't exist.
|
|
92
|
+
*/
|
|
93
|
+
function readFileSafe(filePath: string): string | null {
|
|
94
|
+
try {
|
|
95
|
+
if (fs.existsSync(filePath)) {
|
|
96
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore read errors
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Tool Registration ──────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Register the enhanced write tool that wraps the default with diff rendering.
|
|
108
|
+
*/
|
|
109
|
+
export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
|
|
110
|
+
// We need to re-register a tool with the same name "write" to override it.
|
|
111
|
+
// The approach: register our own tool that reads old content, writes the file,
|
|
112
|
+
// computes the diff, and stores it for rendering.
|
|
113
|
+
|
|
114
|
+
pi.registerTool({
|
|
115
|
+
name: "write",
|
|
116
|
+
label: "Write File",
|
|
117
|
+
description: "Write content to a file at the given path. Creates parent directories if needed. Shows a syntax-highlighted diff of the changes.",
|
|
118
|
+
parameters: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
path: { type: "string", description: "Path to the file to write" },
|
|
122
|
+
content: { type: "string", description: "Content to write to the file" },
|
|
123
|
+
},
|
|
124
|
+
required: ["path", "content"],
|
|
125
|
+
} as any,
|
|
126
|
+
async execute(toolCallId: string, params: any, signal: any, _onUpdate: any, _ctx: any): Promise<any> {
|
|
127
|
+
const { path: filePath, content } = params;
|
|
128
|
+
const absolutePath = path.resolve(cwd, filePath);
|
|
129
|
+
const dir = path.dirname(absolutePath);
|
|
130
|
+
|
|
131
|
+
// Read old content before write
|
|
132
|
+
const oldContent = readFileSafe(absolutePath);
|
|
133
|
+
|
|
134
|
+
// Write the file
|
|
135
|
+
if (!fs.existsSync(dir)) {
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
fs.writeFileSync(absolutePath, content, "utf-8");
|
|
139
|
+
|
|
140
|
+
// Compute diff
|
|
141
|
+
const language = detectLanguageFromPath(filePath);
|
|
142
|
+
const diff = parseDiff(oldContent ?? "", content, 3, filePath, filePath);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{ type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` },
|
|
147
|
+
],
|
|
148
|
+
details: {
|
|
149
|
+
oldContent,
|
|
150
|
+
newContent: content,
|
|
151
|
+
diff,
|
|
152
|
+
filePath,
|
|
153
|
+
language,
|
|
154
|
+
} as DiffToolDetails,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
renderResult(result: any, _options: any, theme: any): any {
|
|
158
|
+
const details = result?.details as DiffToolDetails | undefined;
|
|
159
|
+
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
160
|
+
return null as any;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const dc = resolveDiffColors(theme);
|
|
165
|
+
const tw = termW();
|
|
166
|
+
const max = 60;
|
|
167
|
+
|
|
168
|
+
const rendered: string = tw >= SPLIT_MIN_WIDTH
|
|
169
|
+
? renderSplit(details.diff, details.language, max, dc)
|
|
170
|
+
: renderUnified(details.diff, details.language, max, dc);
|
|
171
|
+
|
|
172
|
+
// Return a simple component-like object with text
|
|
173
|
+
return {
|
|
174
|
+
setText: () => {},
|
|
175
|
+
text: rendered,
|
|
176
|
+
render: () => rendered.split("\n"),
|
|
177
|
+
} as any;
|
|
178
|
+
} catch {
|
|
179
|
+
return null as any;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register the enhanced edit tool that wraps the default with diff rendering.
|
|
187
|
+
*/
|
|
188
|
+
export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
|
|
189
|
+
pi.registerTool({
|
|
190
|
+
name: "edit",
|
|
191
|
+
label: "Edit File",
|
|
192
|
+
description: "Edit a file by replacing text. Shows a syntax-highlighted diff of the changes.",
|
|
193
|
+
parameters: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
path: { type: "string", description: "Path to the file to edit" },
|
|
197
|
+
edits: {
|
|
198
|
+
type: "array",
|
|
199
|
+
items: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
oldText: { type: "string", description: "Text to replace" },
|
|
203
|
+
newText: { type: "string", description: "Replacement text" },
|
|
204
|
+
},
|
|
205
|
+
required: ["oldText", "newText"],
|
|
206
|
+
},
|
|
207
|
+
description: "Array of edit operations",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
required: ["path", "edits"],
|
|
211
|
+
} as any,
|
|
212
|
+
async execute(toolCallId: string, params: any, signal: any, _onUpdate: any, _ctx: any): Promise<any> {
|
|
213
|
+
const { path: filePath, edits } = params;
|
|
214
|
+
const absolutePath = path.resolve(cwd, filePath);
|
|
215
|
+
|
|
216
|
+
// Read old content
|
|
217
|
+
const oldContent = readFileSafe(absolutePath);
|
|
218
|
+
if (oldContent === null) {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
|
|
221
|
+
details: undefined,
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Apply edits
|
|
227
|
+
let newContent = oldContent;
|
|
228
|
+
const operations = getEditOperations(params);
|
|
229
|
+
for (const op of operations) {
|
|
230
|
+
const idx = newContent.indexOf(op.oldText);
|
|
231
|
+
if (idx === -1) {
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `Error: Could not find text to replace in ${filePath}` }],
|
|
234
|
+
details: undefined,
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
newContent = newContent.substring(0, idx) + op.newText + newContent.substring(idx + op.oldText.length);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Write the modified content
|
|
242
|
+
fs.writeFileSync(absolutePath, newContent, "utf-8");
|
|
243
|
+
|
|
244
|
+
// Compute diff
|
|
245
|
+
const language = detectLanguageFromPath(filePath);
|
|
246
|
+
const diff = parseDiff(oldContent, newContent, 3, filePath, filePath);
|
|
247
|
+
const summary = summarizeEditOperations(operations);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{ type: "text", text: `Successfully edited ${filePath} (${summary.totalEdits} edit${summary.totalEdits !== 1 ? "s" : ""})` },
|
|
252
|
+
],
|
|
253
|
+
details: {
|
|
254
|
+
oldContent,
|
|
255
|
+
newContent,
|
|
256
|
+
diff,
|
|
257
|
+
filePath,
|
|
258
|
+
language,
|
|
259
|
+
} as DiffToolDetails,
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
renderResult(result: any, _options: any, theme: any): any {
|
|
263
|
+
const details = result?.details as DiffToolDetails | undefined;
|
|
264
|
+
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
265
|
+
return null as any;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const dc = resolveDiffColors(theme);
|
|
270
|
+
const tw = termW();
|
|
271
|
+
const max = 60;
|
|
272
|
+
|
|
273
|
+
const rendered: string = tw >= SPLIT_MIN_WIDTH
|
|
274
|
+
? renderSplit(details.diff, details.language, max, dc)
|
|
275
|
+
: renderUnified(details.diff, details.language, max, dc);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
setText: () => {},
|
|
279
|
+
text: rendered,
|
|
280
|
+
render: () => rendered.split("\n"),
|
|
281
|
+
} as any;
|
|
282
|
+
} catch {
|
|
283
|
+
return null as any;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
import { registerUtilityCommands, registerNameBadgeCommands } from "./commands.js";
|
|
26
26
|
import { NameBadgeState } from "./tui/name-badge-state.js";
|
|
27
27
|
import { readBadgeSettings } from "./tui/badge-settings.js";
|
|
28
|
+
import { readDiffSettings } from "./diff/settings.js";
|
|
29
|
+
import { registerEnhancedWriteTool, registerEnhancedEditTool } from "./diff/wrapper.js";
|
|
28
30
|
import { getLifecycle } from "./lifecycle/process.js";
|
|
29
31
|
import { getAnalyticsCollector } from "./analytics/collector.js";
|
|
30
32
|
import { registerInfoScreen } from "./info-screen.js";
|
|
@@ -38,6 +40,12 @@ const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
|
38
40
|
/** Whether we've seen the first user message (for auto badge generation) */
|
|
39
41
|
let firstMessageSeen = false;
|
|
40
42
|
|
|
43
|
+
/** Stored user text from first input, used to build conversation summary after agent responds */
|
|
44
|
+
let firstUserText = "";
|
|
45
|
+
|
|
46
|
+
/** Stored UI context from first input, used to show badge overlay after agent responds */
|
|
47
|
+
let firstInputCtx: any = null;
|
|
48
|
+
|
|
41
49
|
/** All commands registered by this module */
|
|
42
50
|
const ALL_COMMANDS = [
|
|
43
51
|
UTILITY_COMMANDS.CONTINUE,
|
|
@@ -50,6 +58,7 @@ const ALL_COMMANDS = [
|
|
|
50
58
|
UTILITY_COMMANDS.BADGE_GEN,
|
|
51
59
|
UTILITY_COMMANDS.BADGE_TOGGLE,
|
|
52
60
|
UTILITY_COMMANDS.BADGE_SETTINGS,
|
|
61
|
+
UTILITY_COMMANDS.UTIL_SETTINGS,
|
|
53
62
|
].map((cmd) => `unipi:${cmd}`);
|
|
54
63
|
|
|
55
64
|
/** All tools registered by this module */
|
|
@@ -96,6 +105,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
96
105
|
// Restore name badge if it was visible in previous session
|
|
97
106
|
await nameBadgeState.restore(pi, ctx);
|
|
98
107
|
|
|
108
|
+
// Register diff-enhanced tools if enabled
|
|
109
|
+
const diffSettings = readDiffSettings();
|
|
110
|
+
if (diffSettings.enabled) {
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
registerEnhancedWriteTool(pi, cwd);
|
|
113
|
+
registerEnhancedEditTool(pi, cwd);
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
// Write model cache for TUI components
|
|
100
117
|
if ((ctx as any).modelRegistry) {
|
|
101
118
|
const { writeModelCache } = await import("@pi-unipi/core");
|
|
@@ -106,7 +123,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
123
|
}
|
|
107
124
|
});
|
|
108
125
|
|
|
109
|
-
// First-message hook:
|
|
126
|
+
// First-message hook: capture user text for deferred badge generation
|
|
110
127
|
pi.on("input", async (_event: any, ctx: any) => {
|
|
111
128
|
// Only trigger on first user message
|
|
112
129
|
if (firstMessageSeen) return;
|
|
@@ -120,8 +137,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
120
137
|
const sessionName = pi.getSessionName?.();
|
|
121
138
|
if (sessionName) return;
|
|
122
139
|
|
|
123
|
-
//
|
|
124
|
-
|
|
140
|
+
// Store first message text for later use in agent_end
|
|
141
|
+
firstUserText = typeof _event?.content === "string"
|
|
125
142
|
? _event.content
|
|
126
143
|
: Array.isArray(_event?.content)
|
|
127
144
|
? _event.content
|
|
@@ -130,16 +147,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
147
|
.join(" ")
|
|
131
148
|
: "";
|
|
132
149
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
// Store ctx for badge overlay show after agent responds
|
|
151
|
+
firstInputCtx = ctx;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// After agent completes first response, generate badge name with full conversation context
|
|
155
|
+
pi.on("agent_end", async (event: any, _ctx: any) => {
|
|
156
|
+
// Only act if we captured a first input and are waiting for badge generation
|
|
157
|
+
if (!firstInputCtx) return;
|
|
158
|
+
const ctx = firstInputCtx;
|
|
159
|
+
firstInputCtx = null; // consume — only trigger once
|
|
160
|
+
|
|
161
|
+
// Check if a name was already set (e.g. manually) in the meantime
|
|
162
|
+
const sessionName = pi.getSessionName?.();
|
|
163
|
+
if (sessionName) return;
|
|
138
164
|
|
|
139
165
|
// Show badge overlay if UI available
|
|
140
166
|
if (ctx?.hasUI && !nameBadgeState.isVisible()) {
|
|
141
167
|
await nameBadgeState.show(pi, ctx);
|
|
142
168
|
}
|
|
169
|
+
|
|
170
|
+
// Build conversation summary from full message history (user + assistant)
|
|
171
|
+
const messages: any[] = event?.messages ?? [];
|
|
172
|
+
const summaryParts: string[] = [];
|
|
173
|
+
|
|
174
|
+
// Include the user's first message
|
|
175
|
+
if (firstUserText) {
|
|
176
|
+
summaryParts.push(`User: ${firstUserText}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Include assistant's response text
|
|
180
|
+
const assistantMsgs = messages.filter((m: any) => m.role === "assistant");
|
|
181
|
+
for (const msg of assistantMsgs) {
|
|
182
|
+
if (Array.isArray(msg.content)) {
|
|
183
|
+
const textParts = msg.content
|
|
184
|
+
.filter((c: any) => c.type === "text")
|
|
185
|
+
.map((c: any) => c.text)
|
|
186
|
+
.join(" ");
|
|
187
|
+
if (textParts) summaryParts.push(`Assistant: ${textParts}`);
|
|
188
|
+
} else if (typeof msg.content === "string" && msg.content) {
|
|
189
|
+
summaryParts.push(`Assistant: ${msg.content}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Truncate to reasonable size
|
|
194
|
+
const conversationSummary = summaryParts.join("\n").slice(0, 800);
|
|
195
|
+
|
|
196
|
+
// Emit event for subagents to spawn background agent
|
|
197
|
+
emitEvent(pi, UNIPI_EVENTS.BADGE_GENERATE_REQUEST, {
|
|
198
|
+
source: "input-hook",
|
|
199
|
+
conversationSummary,
|
|
200
|
+
});
|
|
143
201
|
});
|
|
144
202
|
|
|
145
203
|
// Track command usage
|
|
@@ -153,6 +211,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
153
211
|
pi.on("session_shutdown", async () => {
|
|
154
212
|
nameBadgeState.hide();
|
|
155
213
|
firstMessageSeen = false;
|
|
214
|
+
firstUserText = "";
|
|
215
|
+
firstInputCtx = null;
|
|
156
216
|
await lifecycle.shutdown("session_shutdown");
|
|
157
217
|
});
|
|
158
218
|
}
|
|
@@ -1,78 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/utility — Badge Settings Manager
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Thin wrappers over the unified settings manager (util-settings.json).
|
|
5
|
+
* Existing callers continue to work unchanged.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
readUtilSettings,
|
|
10
|
+
writeUtilSettings,
|
|
11
|
+
type BadgeSettingsSection,
|
|
12
|
+
} from "../diff/settings.js";
|
|
10
13
|
|
|
11
|
-
/** Badge settings interface */
|
|
12
|
-
export
|
|
13
|
-
/** Auto-generate session name on first user message */
|
|
14
|
-
autoGen: boolean;
|
|
15
|
-
/** Show the badge overlay */
|
|
16
|
-
badgeEnabled: boolean;
|
|
17
|
-
/** Enable the set_session_name tool for agents */
|
|
18
|
-
agentTool: boolean;
|
|
19
|
-
/** Model to use for badge name generation. "inherit" = parent model, or "provider/model-id" */
|
|
20
|
-
generationModel: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Default badge settings */
|
|
24
|
-
const DEFAULT_SETTINGS: BadgeSettings = {
|
|
25
|
-
autoGen: true,
|
|
26
|
-
badgeEnabled: true,
|
|
27
|
-
agentTool: true,
|
|
28
|
-
generationModel: "inherit",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Badge settings file name */
|
|
32
|
-
const BADGE_CONFIG_FILE = ".unipi/config/badge.json";
|
|
14
|
+
/** Badge settings interface (re-exports BadgeSettingsSection for backward compat) */
|
|
15
|
+
export type BadgeSettings = BadgeSettingsSection;
|
|
33
16
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
function getConfigPath(): string {
|
|
38
|
-
return path.resolve(process.cwd(), BADGE_CONFIG_FILE);
|
|
39
|
-
}
|
|
17
|
+
/** Default badge settings for formatBadgeSettings display */
|
|
18
|
+
const BADGE_CONFIG_FILE = ".unipi/config/util-settings.json";
|
|
40
19
|
|
|
41
20
|
/**
|
|
42
|
-
* Read badge settings from
|
|
21
|
+
* Read badge settings from unified config.
|
|
43
22
|
* Returns defaults if file doesn't exist or is malformed.
|
|
44
23
|
*/
|
|
45
24
|
export function readBadgeSettings(): BadgeSettings {
|
|
46
|
-
|
|
47
|
-
const configPath = getConfigPath();
|
|
48
|
-
if (!fs.existsSync(configPath)) return { ...DEFAULT_SETTINGS };
|
|
49
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
50
|
-
return {
|
|
51
|
-
autoGen: typeof parsed.autoGen === "boolean" ? parsed.autoGen : DEFAULT_SETTINGS.autoGen,
|
|
52
|
-
badgeEnabled: typeof parsed.badgeEnabled === "boolean" ? parsed.badgeEnabled : DEFAULT_SETTINGS.badgeEnabled,
|
|
53
|
-
agentTool: typeof parsed.agentTool === "boolean" ? parsed.agentTool : DEFAULT_SETTINGS.agentTool,
|
|
54
|
-
generationModel: typeof parsed.generationModel === "string" ? parsed.generationModel : DEFAULT_SETTINGS.generationModel,
|
|
55
|
-
};
|
|
56
|
-
} catch {
|
|
57
|
-
return { ...DEFAULT_SETTINGS };
|
|
58
|
-
}
|
|
25
|
+
return readUtilSettings().badge;
|
|
59
26
|
}
|
|
60
27
|
|
|
61
28
|
/**
|
|
62
|
-
* Write badge settings to
|
|
63
|
-
* Creates .unipi/config/ directory if needed.
|
|
29
|
+
* Write badge settings to unified config.
|
|
64
30
|
*/
|
|
65
31
|
export function writeBadgeSettings(settings: BadgeSettings): void {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(dir)) {
|
|
70
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
fs.writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
73
|
-
} catch {
|
|
74
|
-
// Best effort
|
|
75
|
-
}
|
|
32
|
+
const util = readUtilSettings();
|
|
33
|
+
util.badge = settings;
|
|
34
|
+
writeUtilSettings(util);
|
|
76
35
|
}
|
|
77
36
|
|
|
78
37
|
/**
|