volute 0.1.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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/channel-Q642YUZE.js +90 -0
- package/dist/chunk-5YW4B7CG.js +181 -0
- package/dist/chunk-A5ZJEMHT.js +40 -0
- package/dist/chunk-D424ZQGI.js +31 -0
- package/dist/chunk-GSPKUPKU.js +120 -0
- package/dist/chunk-H5XQARAP.js +48 -0
- package/dist/chunk-KSMIWOCN.js +84 -0
- package/dist/chunk-N4QN44LC.js +74 -0
- package/dist/chunk-XZN4WPNC.js +34 -0
- package/dist/cli.js +95 -0
- package/dist/connect-LW6G23AV.js +48 -0
- package/dist/connectors/discord.js +213 -0
- package/dist/create-3K6O2SDC.js +62 -0
- package/dist/daemon-client-ZTHW7ROS.js +10 -0
- package/dist/daemon.js +1731 -0
- package/dist/delete-JNGY7ZFH.js +54 -0
- package/dist/disconnect-ACVTKTRE.js +30 -0
- package/dist/down-FYCUYC5H.js +71 -0
- package/dist/env-7SLRN3MG.js +159 -0
- package/dist/fork-BB3DZ426.js +112 -0
- package/dist/import-W2AMTEV5.js +410 -0
- package/dist/logs-BUHRIQ2L.js +35 -0
- package/dist/merge-446QTE7Q.js +219 -0
- package/dist/schedule-KKSOVUDF.js +113 -0
- package/dist/send-WQSVSRDD.js +50 -0
- package/dist/start-LKMWS6ZE.js +29 -0
- package/dist/status-CIEKUI3V.js +50 -0
- package/dist/stop-YTOAGYE4.js +29 -0
- package/dist/up-AJJ4GCXY.js +111 -0
- package/dist/upgrade-JACA6YMO.js +211 -0
- package/dist/variants-HPY4DEWU.js +60 -0
- package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
- package/dist/web-assets/index.html +15 -0
- package/package.json +76 -0
- package/templates/_base/.init/MEMORY.md +2 -0
- package/templates/_base/.init/SOUL.md +2 -0
- package/templates/_base/.init/memory/.gitkeep +0 -0
- package/templates/_base/_skills/memory/SKILL.md +30 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
- package/templates/_base/biome.json.tmpl +21 -0
- package/templates/_base/home/VOLUTE.md +19 -0
- package/templates/_base/src/lib/auto-commit.ts +46 -0
- package/templates/_base/src/lib/logger.ts +47 -0
- package/templates/_base/src/lib/types.ts +24 -0
- package/templates/_base/src/lib/volute-server.ts +98 -0
- package/templates/_base/tsconfig.json +13 -0
- package/templates/_base/volute.json.tmpl +3 -0
- package/templates/agent-sdk/.init/CLAUDE.md +36 -0
- package/templates/agent-sdk/package.json.tmpl +20 -0
- package/templates/agent-sdk/src/lib/agent.ts +199 -0
- package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
- package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
- package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
- package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
- package/templates/agent-sdk/src/server.ts +158 -0
- package/templates/agent-sdk/volute-template.json +9 -0
- package/templates/pi/.init/AGENTS.md +26 -0
- package/templates/pi/package.json.tmpl +20 -0
- package/templates/pi/src/lib/agent.ts +205 -0
- package/templates/pi/src/server.ts +121 -0
- package/templates/pi/volute-template.json +9 -0
- package/templates/pi/volute.json.tmpl +3 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getModel, getModels, type ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
AuthStorage,
|
|
4
|
+
createAgentSession,
|
|
5
|
+
DefaultResourceLoader,
|
|
6
|
+
type ExtensionFactory,
|
|
7
|
+
ModelRegistry,
|
|
8
|
+
SessionManager,
|
|
9
|
+
SettingsManager,
|
|
10
|
+
} from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { commitFileChange } from "./auto-commit.js";
|
|
12
|
+
import { log, logMessage, logText, logThinking, logToolResult, logToolUse } from "./logger.js";
|
|
13
|
+
import type { ChannelMeta, VoluteContentPart, VoluteEvent } from "./types.js";
|
|
14
|
+
|
|
15
|
+
type Listener = (event: VoluteEvent) => void;
|
|
16
|
+
|
|
17
|
+
function formatPrefix(meta: ChannelMeta | undefined): string {
|
|
18
|
+
if (!meta?.channel && !meta?.sender) return "";
|
|
19
|
+
const platform =
|
|
20
|
+
meta.platform ??
|
|
21
|
+
(() => {
|
|
22
|
+
const n = (meta.channel ?? "").split(":")[0];
|
|
23
|
+
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
24
|
+
})();
|
|
25
|
+
let sender = meta.sender ?? "";
|
|
26
|
+
if (meta.isDM) {
|
|
27
|
+
sender += " in DM";
|
|
28
|
+
} else if (meta.channelName) {
|
|
29
|
+
sender += ` in #${meta.channelName}`;
|
|
30
|
+
if (meta.guildName) sender += ` in ${meta.guildName}`;
|
|
31
|
+
}
|
|
32
|
+
const parts = [platform, sender].filter(Boolean);
|
|
33
|
+
return parts.length > 0 ? `[${parts.join(": ")}]\n` : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createAgent(options: {
|
|
37
|
+
systemPrompt: string;
|
|
38
|
+
cwd: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
resume?: boolean;
|
|
41
|
+
onCompact?: () => void;
|
|
42
|
+
}) {
|
|
43
|
+
const listeners = new Set<Listener>();
|
|
44
|
+
|
|
45
|
+
// Block compaction once so the agent can update its daily log with full context
|
|
46
|
+
let compactBlocked = false;
|
|
47
|
+
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
48
|
+
pi.on("session_before_compact", () => {
|
|
49
|
+
if (!compactBlocked) {
|
|
50
|
+
compactBlocked = true;
|
|
51
|
+
log("agent", "blocking compaction — asking agent to update daily log first");
|
|
52
|
+
if (options.onCompact) options.onCompact();
|
|
53
|
+
return { cancel: true };
|
|
54
|
+
}
|
|
55
|
+
compactBlocked = false;
|
|
56
|
+
log("agent", "allowing compaction");
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Parse model string: "provider:model-id" or use default
|
|
61
|
+
const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
|
|
62
|
+
const [provider, ...rest] = modelStr.split(":");
|
|
63
|
+
const modelId = rest.join(":");
|
|
64
|
+
|
|
65
|
+
// Try exact match first, then prefix match against available models
|
|
66
|
+
let model = getModel(provider as any, modelId as any);
|
|
67
|
+
if (!model) {
|
|
68
|
+
const available = getModels(provider as any);
|
|
69
|
+
const found = available.find((m) => m.id.startsWith(modelId));
|
|
70
|
+
if (found) model = found;
|
|
71
|
+
}
|
|
72
|
+
if (!model) {
|
|
73
|
+
const available = getModels(provider as any);
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Model not found: ${modelStr}\nAvailable ${provider} models: ${available.map((m) => m.id).join(", ")}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const authStorage = new AuthStorage();
|
|
80
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
81
|
+
|
|
82
|
+
// Use persistent sessions in cwd for resume support, or in-memory if not resuming
|
|
83
|
+
const sessionManager = options.resume
|
|
84
|
+
? SessionManager.continueRecent(options.cwd)
|
|
85
|
+
: SessionManager.create(options.cwd);
|
|
86
|
+
|
|
87
|
+
const settingsManager = SettingsManager.inMemory({
|
|
88
|
+
retry: { enabled: true, maxRetries: 3 },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
92
|
+
cwd: options.cwd,
|
|
93
|
+
settingsManager,
|
|
94
|
+
systemPrompt: options.systemPrompt,
|
|
95
|
+
extensionFactories: [preCompactExtension],
|
|
96
|
+
});
|
|
97
|
+
await resourceLoader.reload();
|
|
98
|
+
|
|
99
|
+
const { session } = await createAgentSession({
|
|
100
|
+
cwd: options.cwd,
|
|
101
|
+
model,
|
|
102
|
+
authStorage,
|
|
103
|
+
modelRegistry,
|
|
104
|
+
sessionManager,
|
|
105
|
+
settingsManager,
|
|
106
|
+
resourceLoader,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function broadcast(event: VoluteEvent) {
|
|
110
|
+
for (const listener of listeners) {
|
|
111
|
+
try {
|
|
112
|
+
listener(event);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log("agent", "listener threw during broadcast:", err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const toolArgs = new Map<string, any>();
|
|
120
|
+
|
|
121
|
+
session.subscribe((event) => {
|
|
122
|
+
if (event.type === "message_update") {
|
|
123
|
+
const ae = event.assistantMessageEvent;
|
|
124
|
+
if (ae.type === "text_delta") {
|
|
125
|
+
logText(ae.delta);
|
|
126
|
+
broadcast({ type: "text", content: ae.delta });
|
|
127
|
+
} else if (ae.type === "thinking_delta") {
|
|
128
|
+
logThinking(ae.delta);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (event.type === "tool_execution_start") {
|
|
133
|
+
toolArgs.set(event.toolCallId, event.args);
|
|
134
|
+
logToolUse(event.toolName, event.args);
|
|
135
|
+
broadcast({
|
|
136
|
+
type: "tool_use",
|
|
137
|
+
name: event.toolName,
|
|
138
|
+
input: event.args,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (event.type === "tool_execution_end") {
|
|
143
|
+
const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
144
|
+
logToolResult(event.toolName, output, event.isError);
|
|
145
|
+
broadcast({
|
|
146
|
+
type: "tool_result",
|
|
147
|
+
output,
|
|
148
|
+
is_error: event.isError,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Auto-commit file changes in home/
|
|
152
|
+
if ((event.toolName === "Edit" || event.toolName === "Write") && !event.isError) {
|
|
153
|
+
const args = toolArgs.get(event.toolCallId);
|
|
154
|
+
const filePath = (args as { file_path?: string })?.file_path;
|
|
155
|
+
if (filePath) {
|
|
156
|
+
commitFileChange(filePath, options.cwd);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
toolArgs.delete(event.toolCallId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (event.type === "agent_end") {
|
|
163
|
+
log("agent", "turn done");
|
|
164
|
+
broadcast({ type: "done" });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
|
|
169
|
+
const raw =
|
|
170
|
+
typeof content === "string"
|
|
171
|
+
? content
|
|
172
|
+
: content
|
|
173
|
+
.filter((p) => p.type === "text")
|
|
174
|
+
.map((p) => (p as { text: string }).text)
|
|
175
|
+
.join("\n");
|
|
176
|
+
logMessage("in", raw, meta?.channel);
|
|
177
|
+
|
|
178
|
+
// Build context prefix from channel metadata
|
|
179
|
+
const prefix = formatPrefix(meta);
|
|
180
|
+
const text = prefix + raw;
|
|
181
|
+
|
|
182
|
+
// Convert image parts to pi-ai ImageContent format
|
|
183
|
+
const images: ImageContent[] | undefined =
|
|
184
|
+
typeof content === "string"
|
|
185
|
+
? undefined
|
|
186
|
+
: content
|
|
187
|
+
.filter((p) => p.type === "image")
|
|
188
|
+
.map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
|
|
189
|
+
|
|
190
|
+
const opts = images?.length ? { images } : {};
|
|
191
|
+
|
|
192
|
+
if (session.isStreaming) {
|
|
193
|
+
session.prompt(text, { streamingBehavior: "followUp", ...opts });
|
|
194
|
+
} else {
|
|
195
|
+
session.prompt(text, opts);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onMessage(listener: Listener): () => void {
|
|
200
|
+
listeners.add(listener);
|
|
201
|
+
return () => listeners.delete(listener);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { sendMessage, onMessage };
|
|
205
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createAgent } from "./lib/agent.js";
|
|
4
|
+
import { log } from "./lib/logger.js";
|
|
5
|
+
import { createVoluteServer } from "./lib/volute-server.js";
|
|
6
|
+
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
let port = 4100;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
13
|
+
port = parseInt(args[++i], 10);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { port };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadConfig(): { model?: string } {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(resolve("volute.json"), "utf-8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadFile(path: string): string {
|
|
29
|
+
try {
|
|
30
|
+
return readFileSync(path, "utf-8");
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { port } = parseArgs();
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
const model = config.model;
|
|
39
|
+
const soulPath = resolve("home/SOUL.md");
|
|
40
|
+
const memoryPath = resolve("home/MEMORY.md");
|
|
41
|
+
const volutePath = resolve("home/VOLUTE.md");
|
|
42
|
+
|
|
43
|
+
const soul = loadFile(soulPath);
|
|
44
|
+
if (!soul) {
|
|
45
|
+
console.error(`Could not read soul file: ${soulPath}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const memory = loadFile(memoryPath);
|
|
50
|
+
const volute = loadFile(volutePath);
|
|
51
|
+
|
|
52
|
+
const promptParts = [soul];
|
|
53
|
+
if (volute) promptParts.push(volute);
|
|
54
|
+
if (memory) promptParts.push(`## Memory\n\n${memory}`);
|
|
55
|
+
const systemPrompt = promptParts.join("\n\n---\n\n");
|
|
56
|
+
|
|
57
|
+
// Check if a session directory exists (indicates resumable session)
|
|
58
|
+
const sessionsDir = resolve("home/.pi/sessions");
|
|
59
|
+
const hasExistingSession = existsSync(sessionsDir);
|
|
60
|
+
if (hasExistingSession) {
|
|
61
|
+
log("server", "found existing sessions, will attempt resume");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read name/version from package.json for health endpoint
|
|
65
|
+
let pkgName = "unknown";
|
|
66
|
+
let pkgVersion = "0.0.0";
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
69
|
+
pkgName = pkg.name || pkgName;
|
|
70
|
+
pkgVersion = pkg.version || pkgVersion;
|
|
71
|
+
} catch {}
|
|
72
|
+
|
|
73
|
+
const agent = await createAgent({
|
|
74
|
+
systemPrompt,
|
|
75
|
+
cwd: resolve("home"),
|
|
76
|
+
model,
|
|
77
|
+
resume: hasExistingSession,
|
|
78
|
+
onCompact: () => {
|
|
79
|
+
log("server", "pre-compact — asking agent to update daily log");
|
|
80
|
+
agent.sendMessage(
|
|
81
|
+
"Conversation is about to be compacted. Please update today's daily log with a summary of what we've discussed and accomplished so far, so context is preserved before compaction.",
|
|
82
|
+
"system",
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const server = createVoluteServer({ agent, port, name: pkgName, version: pkgVersion });
|
|
88
|
+
|
|
89
|
+
server.listen(port, () => {
|
|
90
|
+
const addr = server.address();
|
|
91
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
92
|
+
log("server", `listening on :${actualPort}`);
|
|
93
|
+
|
|
94
|
+
// Check for post-merge context
|
|
95
|
+
const mergedPath = resolve(".volute/merged.json");
|
|
96
|
+
if (existsSync(mergedPath)) {
|
|
97
|
+
try {
|
|
98
|
+
const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
|
|
99
|
+
unlinkSync(mergedPath);
|
|
100
|
+
|
|
101
|
+
const parts = [
|
|
102
|
+
`[system] Variant "${merged.name}" has been merged and you have been restarted.`,
|
|
103
|
+
];
|
|
104
|
+
if (merged.summary) parts.push(`Changes: ${merged.summary}`);
|
|
105
|
+
if (merged.justification) parts.push(`Why: ${merged.justification}`);
|
|
106
|
+
if (merged.memory) parts.push(`Context: ${merged.memory}`);
|
|
107
|
+
|
|
108
|
+
agent.sendMessage(parts.join("\n"));
|
|
109
|
+
log("server", `sent post-merge orientation for variant: ${merged.name}`);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
log("server", "failed to process merged.json:", e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function shutdown() {
|
|
117
|
+
log("server", "shutdown signal received");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
process.on("SIGINT", shutdown);
|
|
121
|
+
process.on("SIGTERM", shutdown);
|