@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.
@@ -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
+ });