@cremini/skillpack 1.0.7 → 1.0.9-im.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/README.md +4 -2
- package/dist/cli.js +64 -12
- package/package.json +3 -2
- package/runtime/server/chat-proxy.js +68 -0
- package/runtime/server/dist/adapters/telegram.js +200 -0
- package/runtime/server/dist/adapters/types.js +1 -0
- package/runtime/server/dist/adapters/web.js +176 -0
- package/runtime/server/dist/agent.js +218 -0
- package/runtime/server/dist/index.js +116 -0
- package/runtime/server/package-lock.json +2311 -41
- package/runtime/server/package.json +11 -2
- package/runtime/start.bat +2 -2
- package/runtime/start.sh +1 -1
- package/runtime/web/app.js +17 -3
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AuthStorage, createAgentSession, ModelRegistry, SessionManager, DefaultResourceLoader, } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
const DEBUG = true;
|
|
4
|
+
const log = (...args) => DEBUG && console.log(...args);
|
|
5
|
+
const write = (data) => DEBUG && process.stdout.write(data);
|
|
6
|
+
function getAssistantDiagnostics(message) {
|
|
7
|
+
if (!message || message.role !== "assistant") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const stopReason = message.stopReason ?? "unknown";
|
|
11
|
+
const errorMessage = message.errorMessage ||
|
|
12
|
+
(stopReason === "error" || stopReason === "aborted"
|
|
13
|
+
? `Request ${stopReason}`
|
|
14
|
+
: "");
|
|
15
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
16
|
+
const text = content
|
|
17
|
+
.filter((item) => item?.type === "text")
|
|
18
|
+
.map((item) => item.text || "")
|
|
19
|
+
.join("")
|
|
20
|
+
.trim();
|
|
21
|
+
const toolCalls = content.filter((item) => item?.type === "toolCall").length;
|
|
22
|
+
return { stopReason, errorMessage, hasText: text.length > 0, toolCalls };
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// PackAgent
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export class PackAgent {
|
|
28
|
+
options;
|
|
29
|
+
channels = new Map();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Lazily create (or return existing) session for a channel.
|
|
35
|
+
*/
|
|
36
|
+
async getOrCreateSession(channelId) {
|
|
37
|
+
const existing = this.channels.get(channelId);
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
const { apiKey, rootDir, provider, modelId } = this.options;
|
|
41
|
+
const authStorage = AuthStorage.inMemory({
|
|
42
|
+
[provider]: { type: "api_key", key: apiKey },
|
|
43
|
+
});
|
|
44
|
+
authStorage.setRuntimeApiKey(provider, apiKey);
|
|
45
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
46
|
+
const model = modelRegistry.find(provider, modelId);
|
|
47
|
+
const sessionManager = SessionManager.inMemory();
|
|
48
|
+
const skillsPath = path.resolve(rootDir, "skills");
|
|
49
|
+
log(`[PackAgent] Loading skills from: ${skillsPath}`);
|
|
50
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
51
|
+
cwd: rootDir,
|
|
52
|
+
additionalSkillPaths: [skillsPath],
|
|
53
|
+
});
|
|
54
|
+
await resourceLoader.reload();
|
|
55
|
+
const { session } = await createAgentSession({
|
|
56
|
+
cwd: rootDir,
|
|
57
|
+
authStorage,
|
|
58
|
+
modelRegistry,
|
|
59
|
+
sessionManager,
|
|
60
|
+
resourceLoader,
|
|
61
|
+
model,
|
|
62
|
+
});
|
|
63
|
+
const channelSession = { session, running: false };
|
|
64
|
+
this.channels.set(channelId, channelSession);
|
|
65
|
+
return channelSession;
|
|
66
|
+
}
|
|
67
|
+
async handleMessage(channelId, text, onEvent) {
|
|
68
|
+
const cs = await this.getOrCreateSession(channelId);
|
|
69
|
+
cs.running = true;
|
|
70
|
+
let turnHadVisibleOutput = false;
|
|
71
|
+
// Subscribe to agent events and forward to adapter
|
|
72
|
+
const unsubscribe = cs.session.subscribe((event) => {
|
|
73
|
+
switch (event.type) {
|
|
74
|
+
case "agent_start":
|
|
75
|
+
log("\n=== [AGENT SESSION START] ===");
|
|
76
|
+
log("System Prompt:\n", cs.session.systemPrompt);
|
|
77
|
+
log("============================\n");
|
|
78
|
+
onEvent({ type: "agent_start" });
|
|
79
|
+
break;
|
|
80
|
+
case "message_start":
|
|
81
|
+
log(`\n--- [Message Start: ${event.message?.role}] ---`);
|
|
82
|
+
if (event.message?.role === "user") {
|
|
83
|
+
log(JSON.stringify(event.message.content, null, 2));
|
|
84
|
+
}
|
|
85
|
+
onEvent({ type: "message_start", role: event.message?.role ?? "" });
|
|
86
|
+
break;
|
|
87
|
+
case "message_update":
|
|
88
|
+
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
89
|
+
turnHadVisibleOutput = true;
|
|
90
|
+
write(event.assistantMessageEvent.delta);
|
|
91
|
+
onEvent({
|
|
92
|
+
type: "text_delta",
|
|
93
|
+
delta: event.assistantMessageEvent.delta,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else if (event.assistantMessageEvent?.type === "thinking_delta") {
|
|
97
|
+
turnHadVisibleOutput = true;
|
|
98
|
+
onEvent({
|
|
99
|
+
type: "thinking_delta",
|
|
100
|
+
delta: event.assistantMessageEvent.delta,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case "message_end":
|
|
105
|
+
log(`\n--- [Message End: ${event.message?.role}] ---`);
|
|
106
|
+
if (event.message?.role === "assistant") {
|
|
107
|
+
const diagnostics = getAssistantDiagnostics(event.message);
|
|
108
|
+
if (diagnostics) {
|
|
109
|
+
log(`[Assistant Diagnostics] stopReason=${diagnostics.stopReason} text=${diagnostics.hasText ? "yes" : "no"} toolCalls=${diagnostics.toolCalls}`);
|
|
110
|
+
if (diagnostics.errorMessage) {
|
|
111
|
+
log(`[Assistant Error] ${diagnostics.errorMessage}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
onEvent({ type: "message_end", role: event.message?.role ?? "" });
|
|
116
|
+
break;
|
|
117
|
+
case "tool_execution_start":
|
|
118
|
+
turnHadVisibleOutput = true;
|
|
119
|
+
log(`\n>>> [Tool Start: ${event.toolName}] >>>`);
|
|
120
|
+
log("Args:", JSON.stringify(event.args, null, 2));
|
|
121
|
+
onEvent({
|
|
122
|
+
type: "tool_start",
|
|
123
|
+
toolName: event.toolName,
|
|
124
|
+
toolInput: event.args,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
case "tool_execution_end":
|
|
128
|
+
turnHadVisibleOutput = true;
|
|
129
|
+
log(`<<< [Tool End: ${event.toolName}] <<<`);
|
|
130
|
+
log(`Error: ${event.isError ? "Yes" : "No"}`);
|
|
131
|
+
onEvent({
|
|
132
|
+
type: "tool_end",
|
|
133
|
+
toolName: event.toolName,
|
|
134
|
+
isError: event.isError,
|
|
135
|
+
result: event.result,
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
case "agent_end":
|
|
139
|
+
log("\n=== [AGENT SESSION END] ===\n");
|
|
140
|
+
onEvent({ type: "agent_end" });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
await cs.session.prompt(text);
|
|
146
|
+
const lastMessage = cs.session.state.messages.at(-1);
|
|
147
|
+
const diagnostics = getAssistantDiagnostics(lastMessage);
|
|
148
|
+
if (diagnostics?.errorMessage) {
|
|
149
|
+
return {
|
|
150
|
+
stopReason: diagnostics.stopReason,
|
|
151
|
+
errorMessage: diagnostics.errorMessage,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (diagnostics &&
|
|
155
|
+
!diagnostics.hasText &&
|
|
156
|
+
diagnostics.toolCalls === 0 &&
|
|
157
|
+
!turnHadVisibleOutput) {
|
|
158
|
+
return {
|
|
159
|
+
stopReason: diagnostics.stopReason,
|
|
160
|
+
errorMessage: "Assistant returned no visible output. Check the server logs for details.",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { stopReason: diagnostics?.stopReason ?? "unknown" };
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
cs.running = false;
|
|
167
|
+
unsubscribe();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async handleCommand(command, channelId) {
|
|
171
|
+
switch (command) {
|
|
172
|
+
case "clear": {
|
|
173
|
+
const cs = this.channels.get(channelId);
|
|
174
|
+
if (cs) {
|
|
175
|
+
cs.session.dispose();
|
|
176
|
+
this.channels.delete(channelId);
|
|
177
|
+
}
|
|
178
|
+
return { success: true, message: "Session cleared." };
|
|
179
|
+
}
|
|
180
|
+
case "restart":
|
|
181
|
+
log("[PackAgent] Restart requested");
|
|
182
|
+
// Give a brief delay so the response can be sent
|
|
183
|
+
setTimeout(() => process.exit(0), 500);
|
|
184
|
+
return { success: true, message: "Restarting..." };
|
|
185
|
+
case "shutdown":
|
|
186
|
+
log("[PackAgent] Shutdown requested");
|
|
187
|
+
setTimeout(() => process.exit(0), 500);
|
|
188
|
+
return { success: true, message: "Shutting down..." };
|
|
189
|
+
default:
|
|
190
|
+
return { success: false, message: `Unknown command: ${command}` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
abort(channelId) {
|
|
194
|
+
const cs = this.channels.get(channelId);
|
|
195
|
+
if (cs?.running) {
|
|
196
|
+
cs.session.abort?.();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
isRunning(channelId) {
|
|
200
|
+
return this.channels.get(channelId)?.running ?? false;
|
|
201
|
+
}
|
|
202
|
+
dispose(channelId) {
|
|
203
|
+
const cs = this.channels.get(channelId);
|
|
204
|
+
if (cs) {
|
|
205
|
+
cs.session.dispose();
|
|
206
|
+
this.channels.delete(channelId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Reserved: list all sessions */
|
|
210
|
+
listSessions() {
|
|
211
|
+
// TODO: Implement session persistence and listing
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
/** Reserved: restore a historical session */
|
|
215
|
+
async restoreSession(_sessionId) {
|
|
216
|
+
// TODO: Implement session restoration
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { exec } from "node:child_process";
|
|
7
|
+
import { PackAgent } from "./agent.js";
|
|
8
|
+
import { WebAdapter } from "./adapters/web.js";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Resolve root directory
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// In dev (running from dist/), go up two levels: dist/ → server/ → pack root
|
|
14
|
+
// In production (copied to target), go up one level: dist/ → server/ → pack root
|
|
15
|
+
const serverDir = path.resolve(__dirname, "..");
|
|
16
|
+
const rootDir = process.env.PACK_ROOT || path.resolve(serverDir, "..");
|
|
17
|
+
let dataConfig = {};
|
|
18
|
+
const configPath = path.join(rootDir, "data", "config.json");
|
|
19
|
+
if (fs.existsSync(configPath)) {
|
|
20
|
+
try {
|
|
21
|
+
dataConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
22
|
+
console.log(" Loaded config from data/config.json");
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.warn(" Warning: Failed to parse data/config.json:", err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let apiKey = dataConfig.apiKey || "";
|
|
29
|
+
let provider = dataConfig.provider || "openai";
|
|
30
|
+
// Environment variables override config file
|
|
31
|
+
if (process.env.OPENAI_API_KEY) {
|
|
32
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
33
|
+
provider = "openai";
|
|
34
|
+
}
|
|
35
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
36
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
37
|
+
provider = "anthropic";
|
|
38
|
+
}
|
|
39
|
+
const modelId = provider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Create Express app & HTTP server
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const webDir = fs.existsSync(path.join(rootDir, "web"))
|
|
44
|
+
? path.join(rootDir, "web")
|
|
45
|
+
: path.join(serverDir, "..", "web");
|
|
46
|
+
const app = express();
|
|
47
|
+
app.use(express.json());
|
|
48
|
+
app.use(express.static(webDir));
|
|
49
|
+
const server = createServer(app);
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Create PackAgent (shared instance)
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
const agent = new PackAgent({ apiKey, rootDir, provider, modelId });
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Start adapters
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
async function startAdapters() {
|
|
58
|
+
// Web adapter is always enabled
|
|
59
|
+
const webAdapter = new WebAdapter();
|
|
60
|
+
await webAdapter.start({ agent, server, app, rootDir });
|
|
61
|
+
// Telegram adapter (conditional)
|
|
62
|
+
if (dataConfig.adapters?.telegram?.token) {
|
|
63
|
+
try {
|
|
64
|
+
const { TelegramAdapter } = await import("./adapters/telegram.js");
|
|
65
|
+
const telegramAdapter = new TelegramAdapter({
|
|
66
|
+
token: dataConfig.adapters.telegram.token,
|
|
67
|
+
});
|
|
68
|
+
await telegramAdapter.start({ agent, server, app, rootDir });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error("[Telegram] Failed to start:", err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Listen
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
79
|
+
const DEFAULT_PORT = 26313;
|
|
80
|
+
server.once("listening", () => {
|
|
81
|
+
const address = server.address();
|
|
82
|
+
const actualPort = typeof address === "string" ? address : address?.port;
|
|
83
|
+
const url = `http://${HOST}:${actualPort}`;
|
|
84
|
+
console.log(`\n Skills Pack Server`);
|
|
85
|
+
console.log(` Running at ${url}\n`);
|
|
86
|
+
// Open the browser automatically
|
|
87
|
+
const cmd = process.platform === "darwin"
|
|
88
|
+
? `open ${url}`
|
|
89
|
+
: process.platform === "win32"
|
|
90
|
+
? `start ${url}`
|
|
91
|
+
: `xdg-open ${url}`;
|
|
92
|
+
exec(cmd, () => { });
|
|
93
|
+
});
|
|
94
|
+
function tryListen(port) {
|
|
95
|
+
server.listen(port, HOST);
|
|
96
|
+
server.once("error", (err) => {
|
|
97
|
+
if (err.code === "EADDRINUSE") {
|
|
98
|
+
console.log(` Port ${port} is in use, trying ${port + 1}...`);
|
|
99
|
+
server.close();
|
|
100
|
+
tryListen(port + 1);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Start adapters, then listen
|
|
108
|
+
startAdapters()
|
|
109
|
+
.then(() => {
|
|
110
|
+
const startPort = Number(process.env.PORT) || DEFAULT_PORT;
|
|
111
|
+
tryListen(startPort);
|
|
112
|
+
})
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
console.error("Failed to start adapters:", err);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|