@heyamiko/amiko-cli 0.4.12 → 0.5.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.
package/dist/app.js DELETED
@@ -1,355 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
4
- import { getModels, getProviders } from "@mariozechner/pi-ai";
5
- import { parseCommand } from "./commands.js";
6
- import { qmdCollectionAdd, qmdEmbed, qmdGet, qmdSearch } from "./memory/qmd.js";
7
- import { buildPrompt } from "./prompt.js";
8
- import { appendSession } from "./session.js";
9
- import { readWorkspaceFiles } from "./workspace.js";
10
- import { createPiAgent } from "./agent/piAgent.js";
11
- import { ModelPicker } from "./components/ModelPicker.js";
12
- import { saveConfigFile } from "./configFile.js";
13
- export function App({ config, workspace, sessionPath }) {
14
- const { exit } = useApp();
15
- const { stdout } = useStdout();
16
- const [input, setInput] = useState("");
17
- const [messages, setMessages] = useState([]);
18
- const [streamingAssistant, setStreamingAssistant] = useState("");
19
- const [status, setStatus] = useState("ready");
20
- const [agentReady, setAgentReady] = useState(false);
21
- const [currentModel, setCurrentModel] = useState(() => {
22
- if (config.model)
23
- return config.model;
24
- if (config.modelProvider && config.modelId)
25
- return `${config.modelProvider}/${config.modelId}`;
26
- return "auto";
27
- });
28
- const [showModelPicker, setShowModelPicker] = useState(false);
29
- const toolMessageIds = useMemo(() => new Set(), []);
30
- const agentPromise = useMemo(() => createPiAgent(config), [
31
- config.model,
32
- config.modelProvider,
33
- config.modelId,
34
- config.thinkingLevel
35
- ]);
36
- useEffect(() => {
37
- let mounted = true;
38
- agentPromise.then((agent) => {
39
- if (!mounted)
40
- return;
41
- setAgentReady(true);
42
- if (currentModel === "auto") {
43
- const value = agent.getCurrentModel?.();
44
- if (value)
45
- setCurrentModel(value);
46
- }
47
- if (agent.onEvent) {
48
- agent.onEvent((event) => {
49
- if (event?.type === "tool_execution_start") {
50
- const key = `${event.toolCallId ?? ""}:${event.toolName ?? ""}:${JSON.stringify(event.args || {})}`;
51
- if (toolMessageIds.has(key))
52
- return;
53
- toolMessageIds.add(key);
54
- const lines = [`[${event.toolName}] start`];
55
- if (event.args?.command)
56
- lines.push(`command: ${event.args.command}`);
57
- if (event.args?.cwd)
58
- lines.push(`cwd: ${event.args.cwd}`);
59
- if (event.args?.timeoutMs)
60
- lines.push(`timeoutMs: ${event.args.timeoutMs}`);
61
- pushMessage("tool", lines.join("\n"));
62
- return;
63
- }
64
- if (event?.type === "tool_execution_end") {
65
- const key = `${event.toolCallId ?? ""}:${event.toolName ?? ""}:${JSON.stringify(event.args || {})}`;
66
- if (toolMessageIds.has(key + ":end"))
67
- return;
68
- toolMessageIds.add(key + ":end");
69
- const resultText = event.result?.content
70
- ? event.result.content.map((c) => (c.text ? c.text : "")).join("\\n")
71
- : "";
72
- const trimmed = resultText.trim();
73
- const summary = trimmed.length > 0 ? trimmed : "(no output)";
74
- pushMessage("tool", `[${event.toolName}] done\n${summary}`);
75
- }
76
- });
77
- }
78
- });
79
- return () => {
80
- mounted = false;
81
- };
82
- }, [agentPromise, currentModel]);
83
- const pushMessage = useCallback((role, content) => {
84
- setMessages((prev) => [...prev, { role, content }]);
85
- }, []);
86
- const handleSubmit = useCallback(async () => {
87
- const trimmed = input.trim();
88
- if (!trimmed)
89
- return;
90
- setInput("");
91
- const command = parseCommand(trimmed);
92
- if (command) {
93
- await handleCommand(command.name, command.args);
94
- return;
95
- }
96
- pushMessage("user", trimmed);
97
- await appendSession(sessionPath, "user", trimmed);
98
- setStatus("thinking");
99
- setStreamingAssistant("");
100
- const [workspaceData, agent] = await Promise.all([
101
- readWorkspaceFiles(workspace),
102
- agentPromise
103
- ]);
104
- let hits = [];
105
- if (config.qmdEnabled) {
106
- hits = await qmdSearch({
107
- qmdBin: config.qmdBin,
108
- query: trimmed,
109
- limit: config.qmdLimit,
110
- collection: config.qmdCollection
111
- });
112
- }
113
- if (config.qmdFull && hits.length > 0) {
114
- const expanded = [];
115
- for (const hit of hits) {
116
- const docId = hit.path || hit.id;
117
- if (!docId) {
118
- expanded.push(hit);
119
- continue;
120
- }
121
- const text = await qmdGet({ qmdBin: config.qmdBin, docId, full: true });
122
- expanded.push({ ...hit, snippet: text || hit.snippet });
123
- }
124
- hits = expanded;
125
- }
126
- const prompt = buildPrompt({
127
- identity: workspaceData.identity,
128
- personality: workspaceData.personality,
129
- memory: workspaceData.memory,
130
- hits
131
- });
132
- let assistantText = "";
133
- try {
134
- for await (const chunk of agent.streamReply({ prompt, userMessage: trimmed })) {
135
- assistantText += chunk;
136
- setStreamingAssistant((prev) => prev + chunk);
137
- }
138
- }
139
- catch (error) {
140
- const message = error.message || "unknown error";
141
- assistantText += `\n[amiko] ${message}\n`;
142
- setStreamingAssistant((prev) => prev + `\n[amiko] ${message}\n`);
143
- }
144
- if (assistantText) {
145
- await appendSession(sessionPath, "assistant", assistantText);
146
- pushMessage("assistant", assistantText);
147
- }
148
- setStreamingAssistant("");
149
- setStatus("ready");
150
- }, [
151
- input,
152
- agentPromise,
153
- config.qmdBin,
154
- config.qmdEnabled,
155
- config.qmdLimit,
156
- config.qmdFull,
157
- config.qmdCollection,
158
- pushMessage,
159
- sessionPath,
160
- workspace
161
- ]);
162
- const handleCommand = useCallback(async (name, args) => {
163
- switch (name) {
164
- case "exit":
165
- exit();
166
- return;
167
- case "help":
168
- pushMessage("assistant", "Commands: /help /exit /search <query> /memory /session /plan /identity /personality /index [collection] /model [provider[/id]]");
169
- return;
170
- case "memory": {
171
- const data = await readWorkspaceFiles(workspace);
172
- pushMessage("assistant", `MEMORY.md:\n\n${data.memory.trim() || "(empty)"}`);
173
- return;
174
- }
175
- case "session":
176
- pushMessage("assistant", `Session file: ${sessionPath}`);
177
- return;
178
- case "plan":
179
- pushMessage("assistant", "Plan file: PLAN.md");
180
- return;
181
- case "identity": {
182
- const data = await readWorkspaceFiles(workspace);
183
- pushMessage("assistant", `IDENTITY.md:\n\n${data.identity.trim() || "(empty)"}`);
184
- return;
185
- }
186
- case "personality": {
187
- const data = await readWorkspaceFiles(workspace);
188
- pushMessage("assistant", `PERSONALITY.md:\n\n${data.personality.trim() || "(empty)"}`);
189
- return;
190
- }
191
- case "index": {
192
- if (!config.qmdEnabled) {
193
- pushMessage("assistant", "QMD is disabled.");
194
- return;
195
- }
196
- const name = args || config.qmdCollection || "amiko";
197
- const added = await qmdCollectionAdd({
198
- qmdBin: config.qmdBin,
199
- name,
200
- path: workspace.root
201
- });
202
- if (!added) {
203
- pushMessage("assistant", "Failed to add QMD collection.");
204
- return;
205
- }
206
- const embedded = await qmdEmbed({ qmdBin: config.qmdBin, collection: name });
207
- pushMessage("assistant", embedded ? `Indexed QMD collection "${name}".` : `Collection "${name}" added, but embedding failed.`);
208
- return;
209
- }
210
- case "search": {
211
- if (!args) {
212
- pushMessage("assistant", "Usage: /search <query>");
213
- return;
214
- }
215
- if (!config.qmdEnabled) {
216
- pushMessage("assistant", "QMD is disabled.");
217
- return;
218
- }
219
- const hits = await qmdSearch({
220
- qmdBin: config.qmdBin,
221
- query: args,
222
- limit: config.qmdLimit,
223
- collection: config.qmdCollection
224
- });
225
- if (hits.length === 0) {
226
- pushMessage("assistant", "No memory hits.");
227
- return;
228
- }
229
- const formatted = hits
230
- .map((hit, index) => {
231
- const label = hit.title || hit.path || hit.id;
232
- const score = hit.score ? ` (${hit.score.toFixed(2)})` : "";
233
- return `${index + 1}. ${label}${score}`;
234
- })
235
- .join("\n");
236
- pushMessage("assistant", formatted);
237
- return;
238
- }
239
- case "model": {
240
- const agent = await agentPromise;
241
- if (!args) {
242
- setShowModelPicker(true);
243
- return;
244
- }
245
- const parts = args.split(" ").filter(Boolean);
246
- const slashParts = args.includes("/") ? args.split("/") : [];
247
- let provider = parts[0];
248
- let modelId = parts[1];
249
- const providerIndex = Number(provider);
250
- const providers = getProviders().sort();
251
- if (Number.isInteger(providerIndex) && providerIndex > 0 && providerIndex <= providers.length) {
252
- provider = providers[providerIndex - 1];
253
- }
254
- if (slashParts.length >= 2) {
255
- provider = slashParts[0]?.trim();
256
- modelId = slashParts.slice(1).join("/").trim();
257
- }
258
- if (provider && !modelId) {
259
- const models = getModels(provider);
260
- if (models.length === 0) {
261
- pushMessage("assistant", `Unknown provider: ${provider}`);
262
- return;
263
- }
264
- const modelList = models.map((m) => m.id).sort();
265
- pushMessage("assistant", `Models for ${provider}:\n${modelList.join("\n")}`);
266
- return;
267
- }
268
- if (!provider || !modelId) {
269
- pushMessage("assistant", "Usage: /model <provider> [modelId] or /model <provider>/<modelId>");
270
- return;
271
- }
272
- if (!agent.setModel) {
273
- pushMessage("assistant", "Agent does not support model switching.");
274
- return;
275
- }
276
- const result = agent.setModel(provider, modelId);
277
- pushMessage("assistant", result.message);
278
- if (result.ok) {
279
- setCurrentModel(`${provider}/${modelId}`);
280
- await saveConfigFile(workspace.root, {
281
- model: `${provider}/${modelId}`,
282
- modelProvider: provider,
283
- modelId
284
- });
285
- }
286
- return;
287
- }
288
- default:
289
- pushMessage("assistant", `Unknown command: ${name}`);
290
- }
291
- }, [
292
- agentPromise,
293
- config.qmdBin,
294
- config.qmdLimit,
295
- config.qmdCollection,
296
- exit,
297
- pushMessage,
298
- sessionPath,
299
- workspace,
300
- currentModel,
301
- config.qmdEnabled
302
- ]);
303
- useInput((value, key) => {
304
- if (key.return) {
305
- void handleSubmit();
306
- return;
307
- }
308
- if (key.backspace || key.delete) {
309
- setInput((prev) => prev.slice(0, -1));
310
- return;
311
- }
312
- if (key.ctrl && value === "c") {
313
- exit();
314
- return;
315
- }
316
- setInput((prev) => prev + value);
317
- });
318
- const terminalWidth = stdout?.columns ?? 80;
319
- const shortWorkspace = terminalWidth < 90 ? workspace.root.split("/").slice(-2).join("/") : workspace.root;
320
- const shortModel = terminalWidth < 90 && currentModel.includes("/")
321
- ? currentModel.split("/").slice(-2).join("/")
322
- : currentModel;
323
- const maxVisibleMessages = Math.max(4, (stdout?.rows ?? 24) - 12);
324
- const visibleMessages = messages.length > maxVisibleMessages ? messages.slice(messages.length - maxVisibleMessages) : messages;
325
- const staticItems = [
326
- { type: "banner" },
327
- ...visibleMessages.map((message) => ({ type: "message", message }))
328
- ];
329
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Static, { items: staticItems, children: (item, index) => {
330
- if (item.type === "banner") {
331
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "amiko-cli" }), _jsxs(Text, { color: "gray", children: [" ", shortWorkspace] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "blue", children: ["model: ", shortModel] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: agentReady ? "green" : "yellow", children: ["agent: ", agentReady ? "ready" : "loading"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "magenta", children: ["status: ", status] })] })] }, `banner-${index}`));
332
- }
333
- const message = item.message;
334
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: message.role === "user" ? "cyan" : message.role === "tool" ? "yellow" : "green", children: ["[", message.role, "]"] }), _jsx(Text, { children: message.content })] }, `message-${index}`));
335
- } }), streamingAssistant ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "green", children: "[assistant]" }), _jsx(Text, { children: streamingAssistant })] })) : null] }), showModelPicker ? (_jsx(ModelPicker, { currentModel: currentModel, onCancel: () => setShowModelPicker(false), onSelect: async (provider, modelId) => {
336
- const agent = await agentPromise;
337
- if (!agent.setModel) {
338
- pushMessage("assistant", "Agent does not support model switching.");
339
- setShowModelPicker(false);
340
- return;
341
- }
342
- const result = agent.setModel(provider, modelId);
343
- pushMessage("assistant", result.message);
344
- if (result.ok)
345
- setCurrentModel(`${provider}/${modelId}`);
346
- if (result.ok) {
347
- await saveConfigFile(workspace.root, {
348
- model: `${provider}/${modelId}`,
349
- modelProvider: provider,
350
- modelId
351
- });
352
- }
353
- setShowModelPicker(false);
354
- } })) : null, _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "> " }), _jsx(Text, { children: input })] })] }));
355
- }
package/dist/cli.js DELETED
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env node
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { render } from "ink";
4
- import { loadConfig } from "./config.js";
5
- import { initWorkspace } from "./workspace.js";
6
- import { createSessionFile } from "./session.js";
7
- import { App } from "./app.js";
8
- import { loadConfigFile } from "./configFile.js";
9
- import { runHeadless } from "./headless.js";
10
- function parseArgs(argv) {
11
- const overrides = {};
12
- for (let i = 0; i < argv.length; i += 1) {
13
- const arg = argv[i];
14
- switch (arg) {
15
- case "--headless":
16
- overrides.headless = true;
17
- break;
18
- case "--message":
19
- case "-m":
20
- overrides.message = argv[i + 1];
21
- i += 1;
22
- break;
23
- case "--workspace":
24
- case "-w":
25
- overrides.workspacePath = argv[i + 1];
26
- i += 1;
27
- break;
28
- case "--session":
29
- overrides.sessionId = argv[i + 1];
30
- i += 1;
31
- break;
32
- case "--qmd-bin":
33
- overrides.qmdBin = argv[i + 1];
34
- i += 1;
35
- break;
36
- case "--qmd-limit":
37
- overrides.qmdLimit = Number(argv[i + 1]);
38
- i += 1;
39
- break;
40
- case "--qmd-collection":
41
- overrides.qmdCollection = argv[i + 1];
42
- i += 1;
43
- break;
44
- case "--qmd-full":
45
- overrides.qmdFull = true;
46
- break;
47
- case "--no-qmd":
48
- overrides.qmdEnabled = false;
49
- break;
50
- case "--model":
51
- overrides.model = argv[i + 1];
52
- i += 1;
53
- break;
54
- case "--model-provider":
55
- overrides.modelProvider = argv[i + 1];
56
- i += 1;
57
- break;
58
- case "--model-id":
59
- overrides.modelId = argv[i + 1];
60
- i += 1;
61
- break;
62
- case "--thinking":
63
- overrides.thinkingLevel = argv[i + 1];
64
- i += 1;
65
- break;
66
- default:
67
- break;
68
- }
69
- }
70
- return overrides;
71
- }
72
- async function main() {
73
- // Ink's render-border.js calls box.top.repeat(contentWidth) where
74
- // contentWidth = width - 2. When yoga computes a zero-width node (common
75
- // in non-TTY / piped environments), contentWidth goes negative and
76
- // String.repeat() throws. Patch String.repeat to clamp to 0.
77
- const origRepeat = String.prototype.repeat;
78
- String.prototype.repeat = function (count) {
79
- return origRepeat.call(this, Math.max(0, Math.floor(count)));
80
- };
81
- const config = loadConfig();
82
- const overrides = parseArgs(process.argv.slice(2));
83
- const finalConfig = { ...config, ...overrides };
84
- const workspace = await initWorkspace(finalConfig.workspacePath);
85
- const persisted = await loadConfigFile(workspace.root);
86
- const mergedConfig = { ...finalConfig, ...persisted, ...overrides };
87
- const sessionFile = await createSessionFile(workspace.sessionsDir, finalConfig.sessionId);
88
- // Auto-detect non-TTY, --headless flag, or -m single-shot
89
- if (overrides.headless || overrides.message || !process.stdout.isTTY) {
90
- await runHeadless(mergedConfig, workspace, sessionFile.path, overrides.message);
91
- return;
92
- }
93
- render(_jsx(App, { config: mergedConfig, workspace: workspace, sessionPath: sessionFile.path }));
94
- }
95
- main().catch((error) => {
96
- // eslint-disable-next-line no-console
97
- console.error("amiko-cli failed:", error);
98
- process.exit(1);
99
- });
package/dist/commands.js DELETED
@@ -1,22 +0,0 @@
1
- export function parseCommand(input) {
2
- if (!input.startsWith("/"))
3
- return null;
4
- const [command, ...rest] = input.slice(1).split(" ");
5
- const name = command.toLowerCase();
6
- const args = rest.join(" ").trim();
7
- switch (name) {
8
- case "exit":
9
- case "help":
10
- case "memory":
11
- case "session":
12
- case "search":
13
- case "plan":
14
- case "index":
15
- case "identity":
16
- case "personality":
17
- case "model":
18
- return { name, args };
19
- default:
20
- return null;
21
- }
22
- }
@@ -1,63 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { Box, Text, useInput, useStdout } from "ink";
4
- import SelectInput from "ink-select-input";
5
- import { getEnvApiKey, getModels, getProviders } from "@mariozechner/pi-ai";
6
- export function ModelPicker({ currentModel, onSelect, onCancel }) {
7
- const [step, setStep] = useState("provider");
8
- const [provider, setProvider] = useState(null);
9
- const { stdout } = useStdout();
10
- const maxItems = Math.max(6, Math.min(15, (stdout?.rows ?? 24) - 12));
11
- const [currentProvider, currentModelId] = useMemo(() => {
12
- if (!currentModel || currentModel === "auto")
13
- return [null, null];
14
- const parts = currentModel.split("/");
15
- if (parts.length < 2)
16
- return [null, null];
17
- return [parts[0], parts.slice(1).join("/")];
18
- }, [currentModel]);
19
- const providerItems = useMemo(() => {
20
- const providers = getProviders().sort();
21
- return providers.map((p) => {
22
- const hasKey = Boolean(getEnvApiKey(p));
23
- return {
24
- label: `${hasKey ? "[x]" : "[ ]"} ${p}`,
25
- value: p
26
- };
27
- });
28
- }, []);
29
- const providerInitialIndex = useMemo(() => {
30
- if (!currentProvider)
31
- return 0;
32
- const idx = providerItems.findIndex((item) => item.value === currentProvider);
33
- return idx >= 0 ? idx : 0;
34
- }, [providerItems, currentProvider]);
35
- const modelItems = useMemo(() => {
36
- if (!provider)
37
- return [];
38
- return getModels(provider)
39
- .map((m) => ({
40
- label: m.id,
41
- value: m.id
42
- }))
43
- .sort((a, b) => a.label.localeCompare(b.label));
44
- }, [provider]);
45
- const modelInitialIndex = useMemo(() => {
46
- if (!currentModelId)
47
- return 0;
48
- const idx = modelItems.findIndex((item) => item.value === currentModelId);
49
- return idx >= 0 ? idx : 0;
50
- }, [modelItems, currentModelId]);
51
- useInput((_, key) => {
52
- if (key.escape)
53
- onCancel();
54
- });
55
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "Model Picker" }), _jsxs(Text, { color: "gray", children: ["Current: ", currentModel] }), step === "provider" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { children: "Select provider (Esc to cancel)" }), _jsx(SelectInput, { items: providerItems, initialIndex: providerInitialIndex, limit: maxItems, onSelect: (item) => {
56
- setProvider(item.value);
57
- setStep("model");
58
- } })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Select model for ", provider] }), modelItems.length === 0 ? (_jsx(Text, { color: "red", children: "No models found." })) : (_jsx(SelectInput, { items: modelItems, initialIndex: modelInitialIndex, limit: maxItems, onSelect: (item) => {
59
- if (!provider)
60
- return;
61
- onSelect(provider, item.value);
62
- } })), _jsx(Text, { color: "gray", children: "Press Esc to cancel" })] }))] }));
63
- }
package/dist/config.js DELETED
@@ -1,26 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- function defaultWorkspacePath() {
5
- return path.join(os.homedir(), ".amiko");
6
- }
7
- function newSessionId() {
8
- return randomUUID().split("-")[0];
9
- }
10
- export function loadConfig() {
11
- const workspacePath = process.env.AMIKO_WORKSPACE || defaultWorkspacePath();
12
- const sessionId = process.env.AMIKO_SESSION_ID || newSessionId();
13
- return {
14
- workspacePath,
15
- sessionId,
16
- qmdBin: process.env.AMIKO_QMD_BIN || "qmd",
17
- qmdLimit: Number(process.env.AMIKO_QMD_LIMIT || 5),
18
- qmdEnabled: process.env.AMIKO_QMD_ENABLED !== "0",
19
- qmdCollection: process.env.AMIKO_QMD_COLLECTION || undefined,
20
- qmdFull: process.env.AMIKO_QMD_FULL === "1",
21
- model: process.env.AMIKO_MODEL || undefined,
22
- modelProvider: process.env.AMIKO_MODEL_PROVIDER || undefined,
23
- modelId: process.env.AMIKO_MODEL_ID || undefined,
24
- thinkingLevel: process.env.AMIKO_THINKING_LEVEL || undefined
25
- };
26
- }
@@ -1,23 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import os from "node:os";
4
- export function getConfigFilePath(workspacePath) {
5
- const base = workspacePath || path.join(os.homedir(), ".amiko");
6
- return path.join(base, "config.json");
7
- }
8
- export async function loadConfigFile(workspacePath) {
9
- const filePath = getConfigFilePath(workspacePath);
10
- try {
11
- const raw = await fs.readFile(filePath, "utf8");
12
- const parsed = JSON.parse(raw);
13
- return parsed || {};
14
- }
15
- catch {
16
- return {};
17
- }
18
- }
19
- export async function saveConfigFile(workspacePath, config) {
20
- const filePath = getConfigFilePath(workspacePath);
21
- await fs.mkdir(path.dirname(filePath), { recursive: true });
22
- await fs.writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
23
- }