@heyamiko/amiko-cli 0.4.12 → 0.5.1
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 +225 -0
- package/dist/index.js +75792 -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 -99
- 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 -227
- 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 -121
- 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 -243
- 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
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import React, { useMemo, useState } from "react";
|
|
2
|
-
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
-
import SelectInput from "ink-select-input";
|
|
4
|
-
import { getEnvApiKey, getModels, getProviders, type KnownProvider } from "@mariozechner/pi-ai";
|
|
5
|
-
|
|
6
|
-
type Item = {
|
|
7
|
-
label: string;
|
|
8
|
-
value: string;
|
|
9
|
-
key?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type ModelPickerProps = {
|
|
13
|
-
currentModel: string;
|
|
14
|
-
onSelect: (provider: string, modelId: string) => void;
|
|
15
|
-
onCancel: () => void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function ModelPicker({ currentModel, onSelect, onCancel }: ModelPickerProps) {
|
|
19
|
-
const [step, setStep] = useState<"provider" | "model">("provider");
|
|
20
|
-
const [provider, setProvider] = useState<string | null>(null);
|
|
21
|
-
const { stdout } = useStdout();
|
|
22
|
-
const maxItems = Math.max(6, Math.min(15, (stdout?.rows ?? 24) - 12));
|
|
23
|
-
|
|
24
|
-
const [currentProvider, currentModelId] = useMemo(() => {
|
|
25
|
-
if (!currentModel || currentModel === "auto") return [null, null] as const;
|
|
26
|
-
const parts = currentModel.split("/");
|
|
27
|
-
if (parts.length < 2) return [null, null] as const;
|
|
28
|
-
return [parts[0], parts.slice(1).join("/")] as const;
|
|
29
|
-
}, [currentModel]);
|
|
30
|
-
|
|
31
|
-
const providerItems = useMemo<Item[]>(() => {
|
|
32
|
-
const providers = getProviders().sort();
|
|
33
|
-
return providers.map((p) => {
|
|
34
|
-
const hasKey = Boolean(getEnvApiKey(p));
|
|
35
|
-
return {
|
|
36
|
-
label: `${hasKey ? "[x]" : "[ ]"} ${p}`,
|
|
37
|
-
value: p
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
const providerInitialIndex = useMemo(() => {
|
|
43
|
-
if (!currentProvider) return 0;
|
|
44
|
-
const idx = providerItems.findIndex((item) => item.value === currentProvider);
|
|
45
|
-
return idx >= 0 ? idx : 0;
|
|
46
|
-
}, [providerItems, currentProvider]);
|
|
47
|
-
|
|
48
|
-
const modelItems = useMemo<Item[]>(() => {
|
|
49
|
-
if (!provider) return [];
|
|
50
|
-
return getModels(provider as KnownProvider)
|
|
51
|
-
.map((m) => ({
|
|
52
|
-
label: m.id,
|
|
53
|
-
value: m.id
|
|
54
|
-
}))
|
|
55
|
-
.sort((a, b) => a.label.localeCompare(b.label));
|
|
56
|
-
}, [provider]);
|
|
57
|
-
|
|
58
|
-
const modelInitialIndex = useMemo(() => {
|
|
59
|
-
if (!currentModelId) return 0;
|
|
60
|
-
const idx = modelItems.findIndex((item) => item.value === currentModelId);
|
|
61
|
-
return idx >= 0 ? idx : 0;
|
|
62
|
-
}, [modelItems, currentModelId]);
|
|
63
|
-
|
|
64
|
-
useInput((_, key) => {
|
|
65
|
-
if (key.escape) onCancel();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1} marginBottom={1}>
|
|
70
|
-
<Text color="cyan">Model Picker</Text>
|
|
71
|
-
<Text color="gray">Current: {currentModel}</Text>
|
|
72
|
-
|
|
73
|
-
{step === "provider" ? (
|
|
74
|
-
<Box flexDirection="column" marginTop={1}>
|
|
75
|
-
<Text>Select provider (Esc to cancel)</Text>
|
|
76
|
-
<SelectInput
|
|
77
|
-
items={providerItems}
|
|
78
|
-
initialIndex={providerInitialIndex}
|
|
79
|
-
limit={maxItems}
|
|
80
|
-
onSelect={(item) => {
|
|
81
|
-
setProvider(item.value);
|
|
82
|
-
setStep("model");
|
|
83
|
-
}}
|
|
84
|
-
/>
|
|
85
|
-
</Box>
|
|
86
|
-
) : (
|
|
87
|
-
<Box flexDirection="column" marginTop={1}>
|
|
88
|
-
<Text>Select model for {provider}</Text>
|
|
89
|
-
{modelItems.length === 0 ? (
|
|
90
|
-
<Text color="red">No models found.</Text>
|
|
91
|
-
) : (
|
|
92
|
-
<SelectInput
|
|
93
|
-
items={modelItems}
|
|
94
|
-
initialIndex={modelInitialIndex}
|
|
95
|
-
limit={maxItems}
|
|
96
|
-
onSelect={(item) => {
|
|
97
|
-
if (!provider) return;
|
|
98
|
-
onSelect(provider, item.value);
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
)}
|
|
102
|
-
<Text color="gray">Press Esc to cancel</Text>
|
|
103
|
-
</Box>
|
|
104
|
-
)}
|
|
105
|
-
</Box>
|
|
106
|
-
);
|
|
107
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { AppConfig } from "./types.js";
|
|
5
|
-
|
|
6
|
-
function defaultWorkspacePath() {
|
|
7
|
-
return path.join(os.homedir(), ".amiko");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function newSessionId() {
|
|
11
|
-
return randomUUID().split("-")[0];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function loadConfig(): AppConfig {
|
|
15
|
-
const workspacePath = process.env.AMIKO_WORKSPACE || defaultWorkspacePath();
|
|
16
|
-
const sessionId = process.env.AMIKO_SESSION_ID || newSessionId();
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
workspacePath,
|
|
20
|
-
sessionId,
|
|
21
|
-
qmdBin: process.env.AMIKO_QMD_BIN || "qmd",
|
|
22
|
-
qmdLimit: Number(process.env.AMIKO_QMD_LIMIT || 5),
|
|
23
|
-
qmdEnabled: process.env.AMIKO_QMD_ENABLED !== "0",
|
|
24
|
-
qmdCollection: process.env.AMIKO_QMD_COLLECTION || undefined,
|
|
25
|
-
qmdFull: process.env.AMIKO_QMD_FULL === "1",
|
|
26
|
-
model: process.env.AMIKO_MODEL || undefined,
|
|
27
|
-
modelProvider: process.env.AMIKO_MODEL_PROVIDER || undefined,
|
|
28
|
-
modelId: process.env.AMIKO_MODEL_ID || undefined,
|
|
29
|
-
thinkingLevel: (process.env.AMIKO_THINKING_LEVEL as any) || undefined
|
|
30
|
-
};
|
|
31
|
-
}
|
package/src/configFile.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { AppConfig } from "./types.js";
|
|
5
|
-
|
|
6
|
-
export type PersistedConfig = {
|
|
7
|
-
model?: string;
|
|
8
|
-
modelProvider?: string;
|
|
9
|
-
modelId?: string;
|
|
10
|
-
thinkingLevel?: AppConfig["thinkingLevel"];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function getConfigFilePath(workspacePath: string) {
|
|
14
|
-
const base = workspacePath || path.join(os.homedir(), ".amiko");
|
|
15
|
-
return path.join(base, "config.json");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function loadConfigFile(workspacePath: string): Promise<PersistedConfig> {
|
|
19
|
-
const filePath = getConfigFilePath(workspacePath);
|
|
20
|
-
try {
|
|
21
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
22
|
-
const parsed = JSON.parse(raw) as PersistedConfig;
|
|
23
|
-
return parsed || {};
|
|
24
|
-
} catch {
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function saveConfigFile(workspacePath: string, config: PersistedConfig): Promise<void> {
|
|
30
|
-
const filePath = getConfigFilePath(workspacePath);
|
|
31
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
32
|
-
await fs.writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
33
|
-
}
|
package/src/headless.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
import { AppConfig, Message, MemoryHit } from "./types.js";
|
|
3
|
-
import { parseCommand } from "./commands.js";
|
|
4
|
-
import { buildPrompt } from "./prompt.js";
|
|
5
|
-
import { appendSession } from "./session.js";
|
|
6
|
-
import { readWorkspaceFiles, WorkspaceFiles } from "./workspace.js";
|
|
7
|
-
import { createPiAgent } from "./agent/piAgent.js";
|
|
8
|
-
import { qmdSearch, qmdGet, qmdCollectionAdd, qmdEmbed } from "./memory/qmd.js";
|
|
9
|
-
import { saveConfigFile } from "./configFile.js";
|
|
10
|
-
import { getModels, getProviders, type KnownProvider } from "@mariozechner/pi-ai";
|
|
11
|
-
|
|
12
|
-
export async function runHeadless(
|
|
13
|
-
config: AppConfig,
|
|
14
|
-
workspace: WorkspaceFiles,
|
|
15
|
-
sessionPath: string,
|
|
16
|
-
singleMessage?: string,
|
|
17
|
-
) {
|
|
18
|
-
const agent = await createPiAgent(config);
|
|
19
|
-
let currentModel = config.model
|
|
20
|
-
?? (config.modelProvider && config.modelId ? `${config.modelProvider}/${config.modelId}` : "auto");
|
|
21
|
-
|
|
22
|
-
if (currentModel === "auto") {
|
|
23
|
-
const value = agent.getCurrentModel?.();
|
|
24
|
-
if (value) currentModel = value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (agent.onEvent) {
|
|
28
|
-
agent.onEvent((event) => {
|
|
29
|
-
if (event?.type === "tool_execution_start") {
|
|
30
|
-
const lines = [`[tool] ${event.toolName} start`];
|
|
31
|
-
if (event.args?.command) lines.push(` command: ${event.args.command}`);
|
|
32
|
-
console.log(lines.join("\n"));
|
|
33
|
-
}
|
|
34
|
-
if (event?.type === "tool_execution_end") {
|
|
35
|
-
const resultText = event.result?.content
|
|
36
|
-
? event.result.content.map((c: any) => (c.text ? c.text : "")).join("\n")
|
|
37
|
-
: "";
|
|
38
|
-
const summary = resultText.trim() || "(no output)";
|
|
39
|
-
console.log(`[tool] ${event.toolName} done\n${summary}`);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Single-shot mode: process one message and exit
|
|
45
|
-
if (singleMessage) {
|
|
46
|
-
const response = await processMessage(singleMessage, { config, workspace, sessionPath, agent, currentModel });
|
|
47
|
-
if (response !== undefined) process.stdout.write(response.endsWith("\n") ? response : response + "\n");
|
|
48
|
-
process.exit(0);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log(`amiko-cli (headless) | model: ${currentModel} | workspace: ${workspace.root}`);
|
|
52
|
-
console.log("Type a message or /command. Press Ctrl+D to exit.");
|
|
53
|
-
|
|
54
|
-
const rl = createInterface({
|
|
55
|
-
input: process.stdin,
|
|
56
|
-
output: process.stdout,
|
|
57
|
-
prompt: "> ",
|
|
58
|
-
terminal: false,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
rl.prompt();
|
|
62
|
-
|
|
63
|
-
for await (const line of rl) {
|
|
64
|
-
const trimmed = line.trim();
|
|
65
|
-
if (!trimmed) {
|
|
66
|
-
rl.prompt();
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const command = parseCommand(trimmed);
|
|
71
|
-
if (command) {
|
|
72
|
-
await handleCommand(command.name, command.args, {
|
|
73
|
-
config, workspace, sessionPath, agent, currentModel,
|
|
74
|
-
setModel: (m: string) => { currentModel = m; },
|
|
75
|
-
});
|
|
76
|
-
rl.prompt();
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const response = await processMessage(trimmed, { config, workspace, sessionPath, agent, currentModel });
|
|
81
|
-
if (response) {
|
|
82
|
-
process.stdout.write(response.endsWith("\n") ? response : response + "\n");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
rl.prompt();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function processMessage(
|
|
92
|
-
message: string,
|
|
93
|
-
ctx: {
|
|
94
|
-
config: AppConfig;
|
|
95
|
-
workspace: WorkspaceFiles;
|
|
96
|
-
sessionPath: string;
|
|
97
|
-
agent: any;
|
|
98
|
-
currentModel: string;
|
|
99
|
-
},
|
|
100
|
-
): Promise<string> {
|
|
101
|
-
await appendSession(ctx.sessionPath, "user", message);
|
|
102
|
-
|
|
103
|
-
const workspaceData = await readWorkspaceFiles(ctx.workspace);
|
|
104
|
-
|
|
105
|
-
let hits: MemoryHit[] = [];
|
|
106
|
-
if (ctx.config.qmdEnabled) {
|
|
107
|
-
hits = await qmdSearch({
|
|
108
|
-
qmdBin: ctx.config.qmdBin,
|
|
109
|
-
query: message,
|
|
110
|
-
limit: ctx.config.qmdLimit,
|
|
111
|
-
collection: ctx.config.qmdCollection,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (ctx.config.qmdFull && hits.length > 0) {
|
|
116
|
-
const expanded: MemoryHit[] = [];
|
|
117
|
-
for (const hit of hits) {
|
|
118
|
-
const docId = hit.path || hit.id;
|
|
119
|
-
if (!docId) { expanded.push(hit); continue; }
|
|
120
|
-
const text = await qmdGet({ qmdBin: ctx.config.qmdBin, docId, full: true });
|
|
121
|
-
expanded.push({ ...hit, snippet: text || hit.snippet });
|
|
122
|
-
}
|
|
123
|
-
hits = expanded;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const prompt = buildPrompt({
|
|
127
|
-
identity: workspaceData.identity,
|
|
128
|
-
personality: workspaceData.personality,
|
|
129
|
-
memory: workspaceData.memory,
|
|
130
|
-
hits,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
let assistantText = "";
|
|
134
|
-
try {
|
|
135
|
-
for await (const chunk of ctx.agent.streamReply({ prompt, userMessage: message })) {
|
|
136
|
-
assistantText += chunk;
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
const msg = (error as Error).message || "unknown error";
|
|
140
|
-
assistantText += `\n[error] ${msg}\n`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (assistantText) {
|
|
144
|
-
await appendSession(ctx.sessionPath, "assistant", assistantText);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return assistantText;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function handleCommand(
|
|
151
|
-
name: string,
|
|
152
|
-
args: string,
|
|
153
|
-
ctx: {
|
|
154
|
-
config: AppConfig;
|
|
155
|
-
workspace: WorkspaceFiles;
|
|
156
|
-
sessionPath: string;
|
|
157
|
-
agent: any;
|
|
158
|
-
currentModel: string;
|
|
159
|
-
setModel: (m: string) => void;
|
|
160
|
-
},
|
|
161
|
-
) {
|
|
162
|
-
switch (name) {
|
|
163
|
-
case "exit":
|
|
164
|
-
process.exit(0);
|
|
165
|
-
case "help":
|
|
166
|
-
console.log("Commands: /help /exit /search <query> /memory /session /plan /identity /personality /index [collection] /model [provider[/id]]");
|
|
167
|
-
return;
|
|
168
|
-
case "memory": {
|
|
169
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
170
|
-
console.log(`MEMORY.md:\n\n${data.memory.trim() || "(empty)"}`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
case "session":
|
|
174
|
-
console.log(`Session file: ${ctx.sessionPath}`);
|
|
175
|
-
return;
|
|
176
|
-
case "plan":
|
|
177
|
-
console.log("Plan file: PLAN.md");
|
|
178
|
-
return;
|
|
179
|
-
case "identity": {
|
|
180
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
181
|
-
console.log(`IDENTITY.md:\n\n${data.identity.trim() || "(empty)"}`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
case "personality": {
|
|
185
|
-
const data = await readWorkspaceFiles(ctx.workspace);
|
|
186
|
-
console.log(`PERSONALITY.md:\n\n${data.personality.trim() || "(empty)"}`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
case "index": {
|
|
190
|
-
if (!ctx.config.qmdEnabled) { console.log("QMD is disabled."); return; }
|
|
191
|
-
const collName = args || ctx.config.qmdCollection || "amiko";
|
|
192
|
-
const added = await qmdCollectionAdd({ qmdBin: ctx.config.qmdBin, name: collName, path: ctx.workspace.root });
|
|
193
|
-
if (!added) { console.log("Failed to add QMD collection."); return; }
|
|
194
|
-
const embedded = await qmdEmbed({ qmdBin: ctx.config.qmdBin, collection: collName });
|
|
195
|
-
console.log(embedded ? `Indexed QMD collection "${collName}".` : `Collection "${collName}" added, but embedding failed.`);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
case "search": {
|
|
199
|
-
if (!args) { console.log("Usage: /search <query>"); return; }
|
|
200
|
-
if (!ctx.config.qmdEnabled) { console.log("QMD is disabled."); return; }
|
|
201
|
-
const hits = await qmdSearch({ qmdBin: ctx.config.qmdBin, query: args, limit: ctx.config.qmdLimit, collection: ctx.config.qmdCollection });
|
|
202
|
-
if (hits.length === 0) { console.log("No memory hits."); return; }
|
|
203
|
-
const formatted = hits.map((hit, i) => {
|
|
204
|
-
const label = hit.title || hit.path || hit.id;
|
|
205
|
-
const score = hit.score ? ` (${hit.score.toFixed(2)})` : "";
|
|
206
|
-
return `${i + 1}. ${label}${score}`;
|
|
207
|
-
}).join("\n");
|
|
208
|
-
console.log(formatted);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
case "model": {
|
|
212
|
-
if (!args) {
|
|
213
|
-
const providers = getProviders().sort();
|
|
214
|
-
console.log(`Current model: ${ctx.currentModel}\nProviders:\n${providers.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}`);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const slashParts = args.includes("/") ? args.split("/") : [];
|
|
218
|
-
let provider = args.split(" ")[0];
|
|
219
|
-
let modelId = args.split(" ")[1];
|
|
220
|
-
if (slashParts.length >= 2) {
|
|
221
|
-
provider = slashParts[0]?.trim();
|
|
222
|
-
modelId = slashParts.slice(1).join("/").trim();
|
|
223
|
-
}
|
|
224
|
-
if (provider && !modelId) {
|
|
225
|
-
const models = getModels(provider as KnownProvider);
|
|
226
|
-
if (models.length === 0) { console.log(`Unknown provider: ${provider}`); return; }
|
|
227
|
-
console.log(`Models for ${provider}:\n${models.map((m) => m.id).sort().join("\n")}`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (!provider || !modelId) { console.log("Usage: /model <provider>/<modelId>"); return; }
|
|
231
|
-
if (!ctx.agent.setModel) { console.log("Agent does not support model switching."); return; }
|
|
232
|
-
const result = ctx.agent.setModel(provider, modelId);
|
|
233
|
-
console.log(result.message);
|
|
234
|
-
if (result.ok) {
|
|
235
|
-
ctx.setModel(`${provider}/${modelId}`);
|
|
236
|
-
await saveConfigFile(ctx.workspace.root, { model: `${provider}/${modelId}`, modelProvider: provider, modelId });
|
|
237
|
-
}
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
default:
|
|
241
|
-
console.log(`Unknown command: ${name}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/memory/qmd.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { MemoryHit } from "../types.js";
|
|
4
|
-
|
|
5
|
-
const execFileAsync = promisify(execFile);
|
|
6
|
-
|
|
7
|
-
function parseJsonLines(text: string) {
|
|
8
|
-
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
9
|
-
const results: any[] = [];
|
|
10
|
-
for (const line of lines) {
|
|
11
|
-
try {
|
|
12
|
-
results.push(JSON.parse(line));
|
|
13
|
-
} catch {
|
|
14
|
-
// ignore
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return results;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function qmdSearch(options: {
|
|
21
|
-
qmdBin: string;
|
|
22
|
-
query: string;
|
|
23
|
-
limit: number;
|
|
24
|
-
collection?: string;
|
|
25
|
-
}): Promise<MemoryHit[]> {
|
|
26
|
-
const { qmdBin, query, limit, collection } = options;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const args = ["query", "--json", "-n", String(limit)];
|
|
30
|
-
if (collection) {
|
|
31
|
-
args.push("-c", collection);
|
|
32
|
-
}
|
|
33
|
-
args.push(query);
|
|
34
|
-
|
|
35
|
-
const { stdout } = await execFileAsync(qmdBin, [
|
|
36
|
-
...args
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const parsed = JSON.parse(stdout);
|
|
41
|
-
if (Array.isArray(parsed)) {
|
|
42
|
-
return parsed.map(normalizeHit);
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// fall through to JSONL parsing
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return parseJsonLines(stdout).map(normalizeHit);
|
|
49
|
-
} catch {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function qmdGet(options: {
|
|
55
|
-
qmdBin: string;
|
|
56
|
-
docId: string;
|
|
57
|
-
full?: boolean;
|
|
58
|
-
}): Promise<string | null> {
|
|
59
|
-
const { qmdBin, docId, full } = options;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const args = ["get", docId];
|
|
63
|
-
if (full) args.push("--full");
|
|
64
|
-
|
|
65
|
-
const { stdout } = await execFileAsync(qmdBin, args);
|
|
66
|
-
return stdout.trim();
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function qmdCollectionAdd(options: {
|
|
73
|
-
qmdBin: string;
|
|
74
|
-
name: string;
|
|
75
|
-
path: string;
|
|
76
|
-
}): Promise<boolean> {
|
|
77
|
-
const { qmdBin, name, path } = options;
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
await execFileAsync(qmdBin, ["collection", "add", path, "--name", name]);
|
|
81
|
-
return true;
|
|
82
|
-
} catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function qmdEmbed(options: {
|
|
88
|
-
qmdBin: string;
|
|
89
|
-
collection?: string;
|
|
90
|
-
}): Promise<boolean> {
|
|
91
|
-
const { qmdBin, collection } = options;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const args = ["embed"];
|
|
95
|
-
if (collection) args.push("-c", collection);
|
|
96
|
-
await execFileAsync(qmdBin, args);
|
|
97
|
-
return true;
|
|
98
|
-
} catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function normalizeHit(hit: any): MemoryHit {
|
|
104
|
-
return {
|
|
105
|
-
id: String(hit.id ?? hit.docId ?? hit.path ?? "unknown"),
|
|
106
|
-
score: typeof hit.score === "number" ? hit.score : undefined,
|
|
107
|
-
title: hit.title ?? hit.name,
|
|
108
|
-
snippet: hit.snippet ?? hit.text,
|
|
109
|
-
path: hit.path
|
|
110
|
-
};
|
|
111
|
-
}
|
package/src/prompt.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import { MemoryHit } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export function buildPrompt(options: {
|
|
5
|
-
identity: string;
|
|
6
|
-
personality: string;
|
|
7
|
-
memory: string;
|
|
8
|
-
hits: MemoryHit[];
|
|
9
|
-
}) {
|
|
10
|
-
const { identity, personality, memory, hits } = options;
|
|
11
|
-
const hitText = hits
|
|
12
|
-
.map((hit, index) => {
|
|
13
|
-
const title = hit.title || hit.path || hit.id;
|
|
14
|
-
const snippet = hit.snippet || "";
|
|
15
|
-
return `(${index + 1}) ${title}\n${snippet}`.trim();
|
|
16
|
-
})
|
|
17
|
-
.filter(Boolean)
|
|
18
|
-
.join("\n\n");
|
|
19
|
-
|
|
20
|
-
const systemInfo = [
|
|
21
|
-
`platform: ${process.platform}`,
|
|
22
|
-
`arch: ${process.arch}`,
|
|
23
|
-
`cwd: ${process.cwd()}`,
|
|
24
|
-
`home: ${os.homedir()}`
|
|
25
|
-
].join("\n");
|
|
26
|
-
|
|
27
|
-
const toolSpec = [
|
|
28
|
-
"Available tools:",
|
|
29
|
-
"- read: { path: string, maxBytes?: number }",
|
|
30
|
-
"- bash: { command: string, cwd?: string, timeoutMs?: number }",
|
|
31
|
-
"",
|
|
32
|
-
"Tool call rules:",
|
|
33
|
-
"1) Always include the tool name.",
|
|
34
|
-
"2) Always include required fields (read.path, bash.command).",
|
|
35
|
-
"3) Do not call tools with empty arguments.",
|
|
36
|
-
"4) Use bash for shell commands; do not invent other tool names.",
|
|
37
|
-
].join("\n");
|
|
38
|
-
|
|
39
|
-
const sections = [
|
|
40
|
-
"# Identity\n" + identity.trim(),
|
|
41
|
-
"# Personality\n" + personality.trim(),
|
|
42
|
-
"# System Info\n" + systemInfo,
|
|
43
|
-
"# Tooling Policy\n" +
|
|
44
|
-
"You can use tools (read, bash) to answer questions. Prefer tools over asking the user to run commands. " +
|
|
45
|
-
"Only ask for clarification if needed to choose a safe tool or path.",
|
|
46
|
-
"# Tool Schemas\n" + toolSpec,
|
|
47
|
-
"# Long-term Memory\n" + memory.trim()
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
if (hitText.length > 0) {
|
|
51
|
-
sections.push("# Retrieved Memory\n" + hitText);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return sections.join("\n\n").trim();
|
|
55
|
-
}
|
package/src/session.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export type SessionFile = {
|
|
5
|
-
id: string;
|
|
6
|
-
path: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function formatDate(date: Date) {
|
|
10
|
-
const yyyy = date.getFullYear();
|
|
11
|
-
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
12
|
-
const dd = String(date.getDate()).padStart(2, "0");
|
|
13
|
-
return `${yyyy}-${mm}-${dd}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function createSessionFile(sessionsDir: string, sessionId: string): Promise<SessionFile> {
|
|
17
|
-
const date = formatDate(new Date());
|
|
18
|
-
const fileName = `${date}-${sessionId}.md`;
|
|
19
|
-
const sessionPath = path.join(sessionsDir, fileName);
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await fs.access(sessionPath);
|
|
23
|
-
} catch {
|
|
24
|
-
const header = `# Session ${fileName}\n\n`;
|
|
25
|
-
await fs.writeFile(sessionPath, header, "utf8");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { id: sessionId, path: sessionPath };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function appendSession(sessionPath: string, role: "user" | "assistant", content: string) {
|
|
32
|
-
const timestamp = new Date().toISOString();
|
|
33
|
-
const block = `## ${role} @ ${timestamp}\n\n${content}\n\n`;
|
|
34
|
-
await fs.appendFile(sessionPath, block, "utf8");
|
|
35
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export type Role = "user" | "assistant" | "system" | "tool";
|
|
2
|
-
|
|
3
|
-
export type Message = {
|
|
4
|
-
role: Role;
|
|
5
|
-
content: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type MemoryHit = {
|
|
9
|
-
id: string;
|
|
10
|
-
score?: number;
|
|
11
|
-
title?: string;
|
|
12
|
-
snippet?: string;
|
|
13
|
-
path?: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type AgentInput = {
|
|
17
|
-
prompt: string;
|
|
18
|
-
userMessage: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type Agent = {
|
|
22
|
-
streamReply: (input: AgentInput) => AsyncGenerator<string>;
|
|
23
|
-
setModel?: (provider: string, modelId: string) => { ok: boolean; message: string };
|
|
24
|
-
getCurrentModel?: () => string | null;
|
|
25
|
-
onEvent?: (handler: (event: any) => void) => void;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type AppConfig = {
|
|
29
|
-
workspacePath: string;
|
|
30
|
-
sessionId: string;
|
|
31
|
-
qmdBin: string;
|
|
32
|
-
qmdLimit: number;
|
|
33
|
-
qmdEnabled: boolean;
|
|
34
|
-
qmdCollection?: string;
|
|
35
|
-
qmdFull: boolean;
|
|
36
|
-
model?: string;
|
|
37
|
-
modelProvider?: string;
|
|
38
|
-
modelId?: string;
|
|
39
|
-
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
40
|
-
};
|