@heyamiko/amiko-cli 0.4.0 → 0.4.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/PLAN.md +113 -0
- package/bun.lock +596 -0
- package/dist/agent/piAgent.js +239 -0
- package/dist/agent/tools.js +149 -0
- package/dist/app.js +355 -0
- package/dist/cli.js +77 -0
- package/dist/commands.js +22 -0
- package/dist/components/ModelPicker.js +63 -0
- package/dist/config.js +26 -0
- package/dist/configFile.js +23 -0
- package/dist/memory/qmd.js +87 -0
- package/dist/prompt.js +43 -0
- package/dist/session.js +26 -0
- package/dist/types.js +1 -0
- package/dist/workspace.js +42 -0
- package/package.json +16 -27
- package/src/agent/piAgent.ts +266 -0
- package/src/agent/tools.ts +153 -0
- package/src/app.tsx +448 -0
- package/src/cli.tsx +95 -0
- package/src/commands.ts +39 -0
- package/src/components/ModelPicker.tsx +107 -0
- package/src/config.ts +31 -0
- package/src/configFile.ts +33 -0
- package/src/memory/qmd.ts +111 -0
- package/src/prompt.ts +55 -0
- package/src/session.ts +35 -0
- package/src/types.ts +40 -0
- package/src/workspace.ts +58 -0
- package/tsconfig.json +16 -0
- package/README.md +0 -222
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -67563
package/dist/app.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
function parseArgs(argv) {
|
|
10
|
+
const overrides = {};
|
|
11
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
12
|
+
const arg = argv[i];
|
|
13
|
+
switch (arg) {
|
|
14
|
+
case "--workspace":
|
|
15
|
+
case "-w":
|
|
16
|
+
overrides.workspacePath = argv[i + 1];
|
|
17
|
+
i += 1;
|
|
18
|
+
break;
|
|
19
|
+
case "--session":
|
|
20
|
+
overrides.sessionId = argv[i + 1];
|
|
21
|
+
i += 1;
|
|
22
|
+
break;
|
|
23
|
+
case "--qmd-bin":
|
|
24
|
+
overrides.qmdBin = argv[i + 1];
|
|
25
|
+
i += 1;
|
|
26
|
+
break;
|
|
27
|
+
case "--qmd-limit":
|
|
28
|
+
overrides.qmdLimit = Number(argv[i + 1]);
|
|
29
|
+
i += 1;
|
|
30
|
+
break;
|
|
31
|
+
case "--qmd-collection":
|
|
32
|
+
overrides.qmdCollection = argv[i + 1];
|
|
33
|
+
i += 1;
|
|
34
|
+
break;
|
|
35
|
+
case "--qmd-full":
|
|
36
|
+
overrides.qmdFull = true;
|
|
37
|
+
break;
|
|
38
|
+
case "--no-qmd":
|
|
39
|
+
overrides.qmdEnabled = false;
|
|
40
|
+
break;
|
|
41
|
+
case "--model":
|
|
42
|
+
overrides.model = argv[i + 1];
|
|
43
|
+
i += 1;
|
|
44
|
+
break;
|
|
45
|
+
case "--model-provider":
|
|
46
|
+
overrides.modelProvider = argv[i + 1];
|
|
47
|
+
i += 1;
|
|
48
|
+
break;
|
|
49
|
+
case "--model-id":
|
|
50
|
+
overrides.modelId = argv[i + 1];
|
|
51
|
+
i += 1;
|
|
52
|
+
break;
|
|
53
|
+
case "--thinking":
|
|
54
|
+
overrides.thinkingLevel = argv[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return overrides;
|
|
62
|
+
}
|
|
63
|
+
async function main() {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
const overrides = parseArgs(process.argv.slice(2));
|
|
66
|
+
const finalConfig = { ...config, ...overrides };
|
|
67
|
+
const workspace = await initWorkspace(finalConfig.workspacePath);
|
|
68
|
+
const persisted = await loadConfigFile(workspace.root);
|
|
69
|
+
const mergedConfig = { ...finalConfig, ...persisted, ...overrides };
|
|
70
|
+
const sessionFile = await createSessionFile(workspace.sessionsDir, finalConfig.sessionId);
|
|
71
|
+
render(_jsx(App, { config: mergedConfig, workspace: workspace, sessionPath: sessionFile.path }));
|
|
72
|
+
}
|
|
73
|
+
main().catch((error) => {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error("amiko-cli failed:", error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
function parseJsonLines(text) {
|
|
5
|
+
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
try {
|
|
9
|
+
results.push(JSON.parse(line));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// ignore
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
export async function qmdSearch(options) {
|
|
18
|
+
const { qmdBin, query, limit, collection } = options;
|
|
19
|
+
try {
|
|
20
|
+
const args = ["query", "--json", "-n", String(limit)];
|
|
21
|
+
if (collection) {
|
|
22
|
+
args.push("-c", collection);
|
|
23
|
+
}
|
|
24
|
+
args.push(query);
|
|
25
|
+
const { stdout } = await execFileAsync(qmdBin, [
|
|
26
|
+
...args
|
|
27
|
+
]);
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(stdout);
|
|
30
|
+
if (Array.isArray(parsed)) {
|
|
31
|
+
return parsed.map(normalizeHit);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// fall through to JSONL parsing
|
|
36
|
+
}
|
|
37
|
+
return parseJsonLines(stdout).map(normalizeHit);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function qmdGet(options) {
|
|
44
|
+
const { qmdBin, docId, full } = options;
|
|
45
|
+
try {
|
|
46
|
+
const args = ["get", docId];
|
|
47
|
+
if (full)
|
|
48
|
+
args.push("--full");
|
|
49
|
+
const { stdout } = await execFileAsync(qmdBin, args);
|
|
50
|
+
return stdout.trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function qmdCollectionAdd(options) {
|
|
57
|
+
const { qmdBin, name, path } = options;
|
|
58
|
+
try {
|
|
59
|
+
await execFileAsync(qmdBin, ["collection", "add", path, "--name", name]);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function qmdEmbed(options) {
|
|
67
|
+
const { qmdBin, collection } = options;
|
|
68
|
+
try {
|
|
69
|
+
const args = ["embed"];
|
|
70
|
+
if (collection)
|
|
71
|
+
args.push("-c", collection);
|
|
72
|
+
await execFileAsync(qmdBin, args);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function normalizeHit(hit) {
|
|
80
|
+
return {
|
|
81
|
+
id: String(hit.id ?? hit.docId ?? hit.path ?? "unknown"),
|
|
82
|
+
score: typeof hit.score === "number" ? hit.score : undefined,
|
|
83
|
+
title: hit.title ?? hit.name,
|
|
84
|
+
snippet: hit.snippet ?? hit.text,
|
|
85
|
+
path: hit.path
|
|
86
|
+
};
|
|
87
|
+
}
|