@heyamiko/amiko-cli 0.4.11 → 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/index.js +75768 -0
- package/package.json +26 -19
- package/PLAN.md +0 -113
- package/bun.lock +0 -743
- package/dist/__tests__/cli-non-tty.test.js +0 -74
- package/dist/agent/piAgent.js +0 -239
- package/dist/agent/tools.js +0 -149
- package/dist/app.js +0 -355
- package/dist/cli.js +0 -94
- package/dist/commands.js +0 -22
- package/dist/components/ModelPicker.js +0 -63
- package/dist/config.js +0 -26
- package/dist/configFile.js +0 -23
- package/dist/headless.js +0 -218
- package/dist/memory/qmd.js +0 -87
- package/dist/prompt.js +0 -43
- package/dist/session.js +0 -26
- package/dist/types.js +0 -1
- package/dist/workspace.js +0 -42
- package/src/__tests__/cli-non-tty.test.ts +0 -86
- package/src/agent/piAgent.ts +0 -266
- package/src/agent/tools.ts +0 -153
- package/src/app.tsx +0 -448
- package/src/cli.tsx +0 -115
- package/src/commands.ts +0 -39
- package/src/components/ModelPicker.tsx +0 -107
- package/src/config.ts +0 -31
- package/src/configFile.ts +0 -33
- package/src/headless.ts +0 -222
- package/src/memory/qmd.ts +0 -111
- package/src/prompt.ts +0 -55
- package/src/session.ts +0 -35
- package/src/types.ts +0 -40
- package/src/workspace.ts +0 -58
- package/tsconfig.json +0 -16
package/dist/headless.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
import { parseCommand } from "./commands.js";
|
|
3
|
-
import { buildPrompt } from "./prompt.js";
|
|
4
|
-
import { appendSession } from "./session.js";
|
|
5
|
-
import { readWorkspaceFiles } from "./workspace.js";
|
|
6
|
-
import { createPiAgent } from "./agent/piAgent.js";
|
|
7
|
-
import { qmdSearch, qmdGet, qmdCollectionAdd, qmdEmbed } from "./memory/qmd.js";
|
|
8
|
-
import { saveConfigFile } from "./configFile.js";
|
|
9
|
-
import { getModels, getProviders } from "@mariozechner/pi-ai";
|
|
10
|
-
export async function runHeadless(config, workspace, sessionPath) {
|
|
11
|
-
const agent = await createPiAgent(config);
|
|
12
|
-
let currentModel = config.model
|
|
13
|
-
?? (config.modelProvider && config.modelId ? `${config.modelProvider}/${config.modelId}` : "auto");
|
|
14
|
-
if (currentModel === "auto") {
|
|
15
|
-
const value = agent.getCurrentModel?.();
|
|
16
|
-
if (value)
|
|
17
|
-
currentModel = value;
|
|
18
|
-
}
|
|
19
|
-
if (agent.onEvent) {
|
|
20
|
-
agent.onEvent((event) => {
|
|
21
|
-
if (event?.type === "tool_execution_start") {
|
|
22
|
-
const lines = [`[tool] ${event.toolName} start`];
|
|
23
|
-
if (event.args?.command)
|
|
24
|
-
lines.push(` command: ${event.args.command}`);
|
|
25
|
-
console.log(lines.join("\n"));
|
|
26
|
-
}
|
|
27
|
-
if (event?.type === "tool_execution_end") {
|
|
28
|
-
const resultText = event.result?.content
|
|
29
|
-
? event.result.content.map((c) => (c.text ? c.text : "")).join("\n")
|
|
30
|
-
: "";
|
|
31
|
-
const summary = resultText.trim() || "(no output)";
|
|
32
|
-
console.log(`[tool] ${event.toolName} done\n${summary}`);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
console.log(`amiko-cli (headless) | model: ${currentModel} | workspace: ${workspace.root}`);
|
|
37
|
-
console.log("Type a message or /command. Press Ctrl+D to exit.");
|
|
38
|
-
const rl = createInterface({
|
|
39
|
-
input: process.stdin,
|
|
40
|
-
output: process.stdout,
|
|
41
|
-
prompt: "> ",
|
|
42
|
-
terminal: false,
|
|
43
|
-
});
|
|
44
|
-
rl.prompt();
|
|
45
|
-
for await (const line of rl) {
|
|
46
|
-
const trimmed = line.trim();
|
|
47
|
-
if (!trimmed) {
|
|
48
|
-
rl.prompt();
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const command = parseCommand(trimmed);
|
|
52
|
-
if (command) {
|
|
53
|
-
await handleCommand(command.name, command.args, {
|
|
54
|
-
config, workspace, sessionPath, agent, currentModel,
|
|
55
|
-
setModel: (m) => { currentModel = m; },
|
|
56
|
-
});
|
|
57
|
-
rl.prompt();
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
console.log(`[user] ${trimmed}`);
|
|
61
|
-
await appendSession(sessionPath, "user", trimmed);
|
|
62
|
-
const workspaceData = await readWorkspaceFiles(workspace);
|
|
63
|
-
let hits = [];
|
|
64
|
-
if (config.qmdEnabled) {
|
|
65
|
-
hits = await qmdSearch({
|
|
66
|
-
qmdBin: config.qmdBin,
|
|
67
|
-
query: trimmed,
|
|
68
|
-
limit: config.qmdLimit,
|
|
69
|
-
collection: config.qmdCollection,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
if (config.qmdFull && hits.length > 0) {
|
|
73
|
-
const expanded = [];
|
|
74
|
-
for (const hit of hits) {
|
|
75
|
-
const docId = hit.path || hit.id;
|
|
76
|
-
if (!docId) {
|
|
77
|
-
expanded.push(hit);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const text = await qmdGet({ qmdBin: config.qmdBin, docId, full: true });
|
|
81
|
-
expanded.push({ ...hit, snippet: text || hit.snippet });
|
|
82
|
-
}
|
|
83
|
-
hits = expanded;
|
|
84
|
-
}
|
|
85
|
-
const prompt = buildPrompt({
|
|
86
|
-
identity: workspaceData.identity,
|
|
87
|
-
personality: workspaceData.personality,
|
|
88
|
-
memory: workspaceData.memory,
|
|
89
|
-
hits,
|
|
90
|
-
});
|
|
91
|
-
let assistantText = "";
|
|
92
|
-
try {
|
|
93
|
-
for await (const chunk of agent.streamReply({ prompt, userMessage: trimmed })) {
|
|
94
|
-
process.stdout.write(chunk);
|
|
95
|
-
assistantText += chunk;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
const message = error.message || "unknown error";
|
|
100
|
-
assistantText += `\n[error] ${message}\n`;
|
|
101
|
-
console.error(`[error] ${message}`);
|
|
102
|
-
}
|
|
103
|
-
if (!assistantText.endsWith("\n"))
|
|
104
|
-
process.stdout.write("\n");
|
|
105
|
-
if (assistantText) {
|
|
106
|
-
await appendSession(sessionPath, "assistant", assistantText);
|
|
107
|
-
}
|
|
108
|
-
rl.prompt();
|
|
109
|
-
}
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
112
|
-
async function handleCommand(name, args, ctx) {
|
|
113
|
-
switch (name) {
|
|
114
|
-
case "exit":
|
|
115
|
-
process.exit(0);
|
|
116
|
-
case "help":
|
|
117
|
-
console.log("Commands: /help /exit /search <query> /memory /session /plan /identity /personality /index [collection] /model [provider[/id]]");
|
|
118
|
-
return;
|
|
119
|
-
case "memory": {
|
|
120
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
121
|
-
console.log(`MEMORY.md:\n\n${data.memory.trim() || "(empty)"}`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
case "session":
|
|
125
|
-
console.log(`Session file: ${ctx.sessionPath}`);
|
|
126
|
-
return;
|
|
127
|
-
case "plan":
|
|
128
|
-
console.log("Plan file: PLAN.md");
|
|
129
|
-
return;
|
|
130
|
-
case "identity": {
|
|
131
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
132
|
-
console.log(`IDENTITY.md:\n\n${data.identity.trim() || "(empty)"}`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
case "personality": {
|
|
136
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
137
|
-
console.log(`PERSONALITY.md:\n\n${data.personality.trim() || "(empty)"}`);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
case "index": {
|
|
141
|
-
if (!ctx.config.qmdEnabled) {
|
|
142
|
-
console.log("QMD is disabled.");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const collName = args || ctx.config.qmdCollection || "amiko";
|
|
146
|
-
const added = await qmdCollectionAdd({ qmdBin: ctx.config.qmdBin, name: collName, path: ctx.workspace.root });
|
|
147
|
-
if (!added) {
|
|
148
|
-
console.log("Failed to add QMD collection.");
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const embedded = await qmdEmbed({ qmdBin: ctx.config.qmdBin, collection: collName });
|
|
152
|
-
console.log(embedded ? `Indexed QMD collection "${collName}".` : `Collection "${collName}" added, but embedding failed.`);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
case "search": {
|
|
156
|
-
if (!args) {
|
|
157
|
-
console.log("Usage: /search <query>");
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (!ctx.config.qmdEnabled) {
|
|
161
|
-
console.log("QMD is disabled.");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const hits = await qmdSearch({ qmdBin: ctx.config.qmdBin, query: args, limit: ctx.config.qmdLimit, collection: ctx.config.qmdCollection });
|
|
165
|
-
if (hits.length === 0) {
|
|
166
|
-
console.log("No memory hits.");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const formatted = hits.map((hit, i) => {
|
|
170
|
-
const label = hit.title || hit.path || hit.id;
|
|
171
|
-
const score = hit.score ? ` (${hit.score.toFixed(2)})` : "";
|
|
172
|
-
return `${i + 1}. ${label}${score}`;
|
|
173
|
-
}).join("\n");
|
|
174
|
-
console.log(formatted);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
case "model": {
|
|
178
|
-
if (!args) {
|
|
179
|
-
const providers = getProviders().sort();
|
|
180
|
-
console.log(`Current model: ${ctx.currentModel}\nProviders:\n${providers.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}`);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const slashParts = args.includes("/") ? args.split("/") : [];
|
|
184
|
-
let provider = args.split(" ")[0];
|
|
185
|
-
let modelId = args.split(" ")[1];
|
|
186
|
-
if (slashParts.length >= 2) {
|
|
187
|
-
provider = slashParts[0]?.trim();
|
|
188
|
-
modelId = slashParts.slice(1).join("/").trim();
|
|
189
|
-
}
|
|
190
|
-
if (provider && !modelId) {
|
|
191
|
-
const models = getModels(provider);
|
|
192
|
-
if (models.length === 0) {
|
|
193
|
-
console.log(`Unknown provider: ${provider}`);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
console.log(`Models for ${provider}:\n${models.map((m) => m.id).sort().join("\n")}`);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (!provider || !modelId) {
|
|
200
|
-
console.log("Usage: /model <provider>/<modelId>");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (!ctx.agent.setModel) {
|
|
204
|
-
console.log("Agent does not support model switching.");
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const result = ctx.agent.setModel(provider, modelId);
|
|
208
|
-
console.log(result.message);
|
|
209
|
-
if (result.ok) {
|
|
210
|
-
ctx.setModel(`${provider}/${modelId}`);
|
|
211
|
-
await saveConfigFile(ctx.workspace.root, { model: `${provider}/${modelId}`, modelProvider: provider, modelId });
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
default:
|
|
216
|
-
console.log(`Unknown command: ${name}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
package/dist/memory/qmd.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/prompt.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
export function buildPrompt(options) {
|
|
3
|
-
const { identity, personality, memory, hits } = options;
|
|
4
|
-
const hitText = hits
|
|
5
|
-
.map((hit, index) => {
|
|
6
|
-
const title = hit.title || hit.path || hit.id;
|
|
7
|
-
const snippet = hit.snippet || "";
|
|
8
|
-
return `(${index + 1}) ${title}\n${snippet}`.trim();
|
|
9
|
-
})
|
|
10
|
-
.filter(Boolean)
|
|
11
|
-
.join("\n\n");
|
|
12
|
-
const systemInfo = [
|
|
13
|
-
`platform: ${process.platform}`,
|
|
14
|
-
`arch: ${process.arch}`,
|
|
15
|
-
`cwd: ${process.cwd()}`,
|
|
16
|
-
`home: ${os.homedir()}`
|
|
17
|
-
].join("\n");
|
|
18
|
-
const toolSpec = [
|
|
19
|
-
"Available tools:",
|
|
20
|
-
"- read: { path: string, maxBytes?: number }",
|
|
21
|
-
"- bash: { command: string, cwd?: string, timeoutMs?: number }",
|
|
22
|
-
"",
|
|
23
|
-
"Tool call rules:",
|
|
24
|
-
"1) Always include the tool name.",
|
|
25
|
-
"2) Always include required fields (read.path, bash.command).",
|
|
26
|
-
"3) Do not call tools with empty arguments.",
|
|
27
|
-
"4) Use bash for shell commands; do not invent other tool names.",
|
|
28
|
-
].join("\n");
|
|
29
|
-
const sections = [
|
|
30
|
-
"# Identity\n" + identity.trim(),
|
|
31
|
-
"# Personality\n" + personality.trim(),
|
|
32
|
-
"# System Info\n" + systemInfo,
|
|
33
|
-
"# Tooling Policy\n" +
|
|
34
|
-
"You can use tools (read, bash) to answer questions. Prefer tools over asking the user to run commands. " +
|
|
35
|
-
"Only ask for clarification if needed to choose a safe tool or path.",
|
|
36
|
-
"# Tool Schemas\n" + toolSpec,
|
|
37
|
-
"# Long-term Memory\n" + memory.trim()
|
|
38
|
-
];
|
|
39
|
-
if (hitText.length > 0) {
|
|
40
|
-
sections.push("# Retrieved Memory\n" + hitText);
|
|
41
|
-
}
|
|
42
|
-
return sections.join("\n\n").trim();
|
|
43
|
-
}
|
package/dist/session.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
function formatDate(date) {
|
|
4
|
-
const yyyy = date.getFullYear();
|
|
5
|
-
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
6
|
-
const dd = String(date.getDate()).padStart(2, "0");
|
|
7
|
-
return `${yyyy}-${mm}-${dd}`;
|
|
8
|
-
}
|
|
9
|
-
export async function createSessionFile(sessionsDir, sessionId) {
|
|
10
|
-
const date = formatDate(new Date());
|
|
11
|
-
const fileName = `${date}-${sessionId}.md`;
|
|
12
|
-
const sessionPath = path.join(sessionsDir, fileName);
|
|
13
|
-
try {
|
|
14
|
-
await fs.access(sessionPath);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
const header = `# Session ${fileName}\n\n`;
|
|
18
|
-
await fs.writeFile(sessionPath, header, "utf8");
|
|
19
|
-
}
|
|
20
|
-
return { id: sessionId, path: sessionPath };
|
|
21
|
-
}
|
|
22
|
-
export async function appendSession(sessionPath, role, content) {
|
|
23
|
-
const timestamp = new Date().toISOString();
|
|
24
|
-
const block = `## ${role} @ ${timestamp}\n\n${content}\n\n`;
|
|
25
|
-
await fs.appendFile(sessionPath, block, "utf8");
|
|
26
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/workspace.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const DEFAULT_IDENTITY = "# Identity\n\nYou are Amiko, a helpful CLI agent.\n";
|
|
4
|
-
const DEFAULT_PERSONALITY = "# Personality\n\n- Clear, concise, friendly.\n- Ask for clarification when unsure.\n";
|
|
5
|
-
const DEFAULT_MEMORY = "# Memory\n\n";
|
|
6
|
-
async function ensureDir(dirPath) {
|
|
7
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
8
|
-
}
|
|
9
|
-
async function ensureFile(filePath, content) {
|
|
10
|
-
try {
|
|
11
|
-
await fs.access(filePath);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
await fs.writeFile(filePath, content, "utf8");
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export async function initWorkspace(root) {
|
|
18
|
-
const identityPath = path.join(root, "IDENTITY.md");
|
|
19
|
-
const personalityPath = path.join(root, "PERSONALITY.md");
|
|
20
|
-
const memoryPath = path.join(root, "MEMORY.md");
|
|
21
|
-
const sessionsDir = path.join(root, "sessions");
|
|
22
|
-
await ensureDir(root);
|
|
23
|
-
await ensureDir(sessionsDir);
|
|
24
|
-
await ensureFile(identityPath, DEFAULT_IDENTITY);
|
|
25
|
-
await ensureFile(personalityPath, DEFAULT_PERSONALITY);
|
|
26
|
-
await ensureFile(memoryPath, DEFAULT_MEMORY);
|
|
27
|
-
return {
|
|
28
|
-
root,
|
|
29
|
-
identityPath,
|
|
30
|
-
personalityPath,
|
|
31
|
-
memoryPath,
|
|
32
|
-
sessionsDir
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
export async function readWorkspaceFiles(files) {
|
|
36
|
-
const [identity, personality, memory] = await Promise.all([
|
|
37
|
-
fs.readFile(files.identityPath, "utf8"),
|
|
38
|
-
fs.readFile(files.personalityPath, "utf8"),
|
|
39
|
-
fs.readFile(files.memoryPath, "utf8")
|
|
40
|
-
]);
|
|
41
|
-
return { identity, personality, memory };
|
|
42
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const CLI_PATH = join(__dirname, "..", "..", "dist", "cli.js");
|
|
8
|
-
|
|
9
|
-
function runCli(args: string[] = [], stdin?: string): Promise<{ stdout: string; stderr: string; code: number | null }> {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const child = execFile("node", [CLI_PATH, ...args], {
|
|
12
|
-
timeout: 10000,
|
|
13
|
-
env: { ...process.env, HOME: "/tmp/amiko-cli-test" },
|
|
14
|
-
}, (error, stdout, stderr) => {
|
|
15
|
-
resolve({
|
|
16
|
-
stdout: stdout || "",
|
|
17
|
-
stderr: stderr || "",
|
|
18
|
-
code: error?.code === "ETIMEDOUT" ? 0 : (error as any)?.status ?? 0,
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
if (stdin !== undefined) {
|
|
22
|
-
child.stdin?.write(stdin);
|
|
23
|
-
}
|
|
24
|
-
child.stdin?.end();
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("amiko-cli non-TTY behavior", () => {
|
|
29
|
-
it("does not crash with RangeError in piped mode", async () => {
|
|
30
|
-
const result = await runCli();
|
|
31
|
-
const combined = result.stdout + result.stderr;
|
|
32
|
-
|
|
33
|
-
expect(combined).not.toContain("RangeError");
|
|
34
|
-
expect(combined).not.toContain("Invalid count value");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("enters headless mode when stdin is not a TTY", async () => {
|
|
38
|
-
const result = await runCli([], "/help\n");
|
|
39
|
-
const combined = result.stdout + result.stderr;
|
|
40
|
-
|
|
41
|
-
expect(combined).toContain("headless");
|
|
42
|
-
expect(combined).toContain("Commands:");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("--headless flag is recognized", async () => {
|
|
46
|
-
const result = await runCli(["--headless"], "/help\n");
|
|
47
|
-
const combined = result.stdout + result.stderr;
|
|
48
|
-
|
|
49
|
-
expect(combined).toContain("headless");
|
|
50
|
-
expect(combined).toContain("Commands:");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("headless /help lists available commands", async () => {
|
|
54
|
-
const result = await runCli([], "/help\n");
|
|
55
|
-
|
|
56
|
-
expect(result.stdout).toContain("/exit");
|
|
57
|
-
expect(result.stdout).toContain("/memory");
|
|
58
|
-
expect(result.stdout).toContain("/model");
|
|
59
|
-
expect(result.stdout).toContain("/search");
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("String.repeat patch", () => {
|
|
64
|
-
it("clamps negative values to empty string", () => {
|
|
65
|
-
const origRepeat = String.prototype.repeat;
|
|
66
|
-
String.prototype.repeat = function (count: number) {
|
|
67
|
-
return origRepeat.call(this, Math.max(0, Math.floor(count)));
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
expect("─".repeat(-2)).toBe("");
|
|
72
|
-
expect("─".repeat(-1)).toBe("");
|
|
73
|
-
expect("─".repeat(0)).toBe("");
|
|
74
|
-
expect("─".repeat(1)).toBe("─");
|
|
75
|
-
expect("─".repeat(3)).toBe("───");
|
|
76
|
-
expect("x".repeat(2.9)).toBe("xx");
|
|
77
|
-
} finally {
|
|
78
|
-
String.prototype.repeat = origRepeat;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("original String.repeat throws on negative values", () => {
|
|
83
|
-
expect(() => "x".repeat(-2)).toThrow(RangeError);
|
|
84
|
-
expect(() => "x".repeat(-1)).toThrow(RangeError);
|
|
85
|
-
});
|
|
86
|
-
});
|