clankie 0.2.1 → 0.2.3

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.
Files changed (41) hide show
  1. package/README.md +29 -13
  2. package/dist/cli.js +301851 -0
  3. package/dist/koffi-216xhpes.node +0 -0
  4. package/dist/koffi-2erktc37.node +0 -0
  5. package/dist/koffi-2rrez93a.node +0 -0
  6. package/dist/koffi-2wv0r22g.node +0 -0
  7. package/dist/koffi-3kae4xj3.node +0 -0
  8. package/dist/koffi-3rkr2zqv.node +0 -0
  9. package/dist/koffi-abxfktv9.node +0 -0
  10. package/dist/koffi-c67c0c5b.node +0 -0
  11. package/dist/koffi-cnf0q0dx.node +0 -0
  12. package/dist/koffi-df38sqz5.node +0 -0
  13. package/dist/koffi-gfbqb3a0.node +0 -0
  14. package/dist/koffi-kjemmmem.node +0 -0
  15. package/dist/koffi-kkrfq9yv.node +0 -0
  16. package/dist/koffi-mzaqwwqy.node +0 -0
  17. package/dist/koffi-q49fgkeq.node +0 -0
  18. package/dist/koffi-q54bk8bf.node +0 -0
  19. package/dist/koffi-x1790w0j.node +0 -0
  20. package/dist/koffi-yxvjwcj6.node +0 -0
  21. package/package.json +17 -7
  22. package/web-ui-dist/_shell.html +2 -2
  23. package/web-ui-dist/assets/{card-kSKmECr1.js → card-BUP-xovx.js} +1 -1
  24. package/web-ui-dist/assets/extensions-DC620Nmx.js +1 -0
  25. package/web-ui-dist/assets/{index-CXJ3n5rE.js → index-DurjG9O_.js} +1 -1
  26. package/web-ui-dist/assets/{loader-circle-C5ib508E.js → loader-circle-DbOtKfCA.js} +1 -1
  27. package/web-ui-dist/assets/{main-cBOaKYCP.js → main-B2sRcuyZ.js} +8 -8
  28. package/web-ui-dist/assets/{sessions._sessionId-BIeINoSQ.js → sessions._sessionId-BJazw9EJ.js} +1 -1
  29. package/web-ui-dist/assets/{settings-CO37Obvo.js → settings-Bv8oeIho.js} +1 -1
  30. package/web-ui-dist/assets/styles-D2oHO1JL.css +1 -0
  31. package/src/agent.ts +0 -107
  32. package/src/channels/channel.ts +0 -57
  33. package/src/channels/slack.ts +0 -374
  34. package/src/channels/web.ts +0 -1362
  35. package/src/cli.ts +0 -505
  36. package/src/config.ts +0 -257
  37. package/src/daemon.ts +0 -380
  38. package/src/service.ts +0 -372
  39. package/src/sessions.ts +0 -251
  40. package/web-ui-dist/assets/extensions-CFPfugfg.js +0 -1
  41. package/web-ui-dist/assets/styles-BQfA8H-l.css +0 -1
package/src/service.ts DELETED
@@ -1,372 +0,0 @@
1
- /**
2
- * clankie service installer — manages systemd (Linux) and launchd (macOS) services.
3
- *
4
- * Commands:
5
- * clankie daemon install — install and start the service
6
- * clankie daemon uninstall — stop and remove the service
7
- * clankie daemon logs — show service logs
8
- *
9
- * On Linux: installs a systemd user service (~/.config/systemd/user/clankie.service)
10
- * On macOS: installs a launchd user agent (~/Library/LaunchAgents/ai.clankie.daemon.plist)
11
- */
12
-
13
- import { execSync } from "node:child_process";
14
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
15
- import { homedir, platform } from "node:os";
16
- import { dirname, join } from "node:path";
17
- import { getAppDir } from "./config.ts";
18
-
19
- const SERVICE_NAME = "clankie";
20
- const LAUNCHD_LABEL = "ai.clankie.daemon";
21
-
22
- // ─── Resolve the app binary path ──────────────────────────────────────────────
23
-
24
- function _resolveAppBinary(): string {
25
- // If running from a compiled binary, use its path
26
- if (!process.argv[1]?.endsWith(".ts")) {
27
- return process.argv[0];
28
- }
29
-
30
- // Running from source — use bun + script path
31
- // Return the full command that systemd/launchd will use
32
- return process.argv[0]; // bun binary path
33
- }
34
-
35
- function resolveProgramArguments(): string[] {
36
- if (!process.argv[1]?.endsWith(".ts")) {
37
- // Compiled binary
38
- return [process.argv[0], "start", "--foreground"];
39
- }
40
- // Running from source with bun
41
- return [process.argv[0], process.argv[1], "start", "--foreground"];
42
- }
43
-
44
- // ─── Systemd (Linux) ──────────────────────────────────────────────────────────
45
-
46
- function systemdUnitPath(): string {
47
- return join(homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
48
- }
49
-
50
- function buildSystemdUnit(): string {
51
- const args = resolveProgramArguments();
52
- const execStart = args.map(systemdEscapeArg).join(" ");
53
- const workspace = join(getAppDir(), "workspace");
54
- const logDir = join(getAppDir(), "logs");
55
-
56
- return [
57
- "[Unit]",
58
- `Description=clankie — personal AI assistant daemon`,
59
- "After=network-online.target",
60
- "Wants=network-online.target",
61
- "",
62
- "[Service]",
63
- `ExecStart=${execStart}`,
64
- `WorkingDirectory=${workspace}`,
65
- "Restart=always",
66
- "RestartSec=5",
67
- "KillMode=process",
68
- `Environment=HOME=${homedir()}`,
69
- `Environment=PATH=${process.env.PATH}`,
70
- `StandardOutput=append:${join(logDir, "daemon.log")}`,
71
- `StandardError=append:${join(logDir, "daemon.log")}`,
72
- "",
73
- "[Install]",
74
- "WantedBy=default.target",
75
- "",
76
- ].join("\n");
77
- }
78
-
79
- function systemdEscapeArg(value: string): string {
80
- if (!/[\s"\\]/.test(value)) return value;
81
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
82
- }
83
-
84
- function execSafe(cmd: string): { ok: boolean; stdout: string; stderr: string } {
85
- try {
86
- const stdout = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
87
- return { ok: true, stdout: stdout.trim(), stderr: "" };
88
- } catch (err: unknown) {
89
- const error = err as { stdout?: string; stderr?: string };
90
- return { ok: false, stdout: error.stdout?.trim() ?? "", stderr: error.stderr?.trim() ?? "" };
91
- }
92
- }
93
-
94
- async function installSystemd(): Promise<void> {
95
- // Check systemctl is available
96
- const check = execSafe("systemctl --user status");
97
- if (!check.ok) {
98
- const detail = `${check.stderr} ${check.stdout}`.toLowerCase();
99
- if (detail.includes("not found") || detail.includes("no such file")) {
100
- console.error("systemctl not found. systemd user services are required on Linux.");
101
- process.exit(1);
102
- }
103
- }
104
-
105
- const unitPath = systemdUnitPath();
106
- const logDir = join(getAppDir(), "logs");
107
- const workspace = join(getAppDir(), "workspace");
108
-
109
- // Ensure directories exist
110
- mkdirSync(dirname(unitPath), { recursive: true });
111
- mkdirSync(logDir, { recursive: true });
112
- mkdirSync(workspace, { recursive: true });
113
-
114
- // Write unit file
115
- const unit = buildSystemdUnit();
116
- writeFileSync(unitPath, unit, "utf-8");
117
- console.log(`Wrote systemd unit: ${unitPath}`);
118
-
119
- // Enable lingering so services run without an active login session
120
- const linger = execSafe("loginctl enable-linger");
121
- if (linger.ok) {
122
- console.log("Enabled user linger (service runs without active login).");
123
- }
124
-
125
- // Reload, enable, and start
126
- const reload = execSafe("systemctl --user daemon-reload");
127
- if (!reload.ok) {
128
- console.error(`daemon-reload failed: ${reload.stderr}`);
129
- process.exit(1);
130
- }
131
-
132
- const enable = execSafe(`systemctl --user enable ${SERVICE_NAME}.service`);
133
- if (!enable.ok) {
134
- console.error(`enable failed: ${enable.stderr}`);
135
- process.exit(1);
136
- }
137
-
138
- const restart = execSafe(`systemctl --user restart ${SERVICE_NAME}.service`);
139
- if (!restart.ok) {
140
- console.error(`restart failed: ${restart.stderr}`);
141
- process.exit(1);
142
- }
143
-
144
- console.log(`\n✓ Installed and started systemd service: ${SERVICE_NAME}.service`);
145
- console.log(` Logs: journalctl --user -u ${SERVICE_NAME} -f`);
146
- console.log(` Or: ${join(logDir, "daemon.log")}`);
147
- }
148
-
149
- async function uninstallSystemd(): Promise<void> {
150
- const unitPath = systemdUnitPath();
151
-
152
- execSafe(`systemctl --user disable --now ${SERVICE_NAME}.service`);
153
-
154
- try {
155
- unlinkSync(unitPath);
156
- console.log(`Removed: ${unitPath}`);
157
- } catch {
158
- console.log(`Service file not found at ${unitPath}`);
159
- }
160
-
161
- execSafe("systemctl --user daemon-reload");
162
- console.log(`✓ Uninstalled systemd service.`);
163
- }
164
-
165
- function logsSystemd(): void {
166
- const logFile = join(getAppDir(), "logs", "daemon.log");
167
- console.log(`Log file: ${logFile}\n`);
168
-
169
- // Try journalctl first, fall back to log file
170
- const result = execSafe(`journalctl --user -u ${SERVICE_NAME} --no-pager -n 50`);
171
- if (result.ok && result.stdout) {
172
- console.log(result.stdout);
173
- } else if (existsSync(logFile)) {
174
- const content = readFileSync(logFile, "utf-8");
175
- const lines = content.split("\n");
176
- const last50 = lines.slice(-50).join("\n");
177
- console.log(last50);
178
- } else {
179
- console.log("No logs found.");
180
- }
181
- }
182
-
183
- function statusSystemd(): void {
184
- const result = execSafe(`systemctl --user status ${SERVICE_NAME}.service`);
185
- console.log(result.stdout || result.stderr || "Service not found.");
186
- }
187
-
188
- // ─── launchd (macOS) ──────────────────────────────────────────────────────────
189
-
190
- function launchdPlistPath(): string {
191
- return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
192
- }
193
-
194
- function plistEscape(value: string): string {
195
- return value
196
- .replace(/&/g, "&amp;")
197
- .replace(/</g, "&lt;")
198
- .replace(/>/g, "&gt;")
199
- .replace(/"/g, "&quot;")
200
- .replace(/'/g, "&apos;");
201
- }
202
-
203
- function buildLaunchdPlist(): string {
204
- const args = resolveProgramArguments();
205
- const logDir = join(getAppDir(), "logs");
206
- const workspace = join(getAppDir(), "workspace");
207
-
208
- const argsXml = args.map((a) => ` <string>${plistEscape(a)}</string>`).join("\n");
209
-
210
- // Build environment variables
211
- const envVars: Record<string, string> = {};
212
- if (process.env.PATH) envVars.PATH = process.env.PATH;
213
- if (process.env.HOME) envVars.HOME = process.env.HOME;
214
-
215
- const envXml = Object.entries(envVars)
216
- .map(([k, v]) => ` <key>${plistEscape(k)}</key>\n <string>${plistEscape(v)}</string>`)
217
- .join("\n");
218
-
219
- return `<?xml version="1.0" encoding="UTF-8"?>
220
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
221
- <plist version="1.0">
222
- <dict>
223
- <key>Label</key>
224
- <string>${plistEscape(LAUNCHD_LABEL)}</string>
225
- <key>RunAtLoad</key>
226
- <true/>
227
- <key>KeepAlive</key>
228
- <true/>
229
- <key>ProgramArguments</key>
230
- <array>
231
- ${argsXml}
232
- </array>
233
- <key>WorkingDirectory</key>
234
- <string>${plistEscape(workspace)}</string>
235
- <key>StandardOutPath</key>
236
- <string>${plistEscape(join(logDir, "daemon.log"))}</string>
237
- <key>StandardErrorPath</key>
238
- <string>${plistEscape(join(logDir, "daemon.log"))}</string>
239
- <key>EnvironmentVariables</key>
240
- <dict>
241
- ${envXml}
242
- </dict>
243
- </dict>
244
- </plist>
245
- `;
246
- }
247
-
248
- async function installLaunchd(): Promise<void> {
249
- const plistPath = launchdPlistPath();
250
- const logDir = join(getAppDir(), "logs");
251
- const workspace = join(getAppDir(), "workspace");
252
-
253
- mkdirSync(dirname(plistPath), { recursive: true });
254
- mkdirSync(logDir, { recursive: true });
255
- mkdirSync(workspace, { recursive: true });
256
-
257
- // Unload existing if present
258
- if (existsSync(plistPath)) {
259
- execSafe(`launchctl unload "${plistPath}"`);
260
- }
261
-
262
- const plist = buildLaunchdPlist();
263
- writeFileSync(plistPath, plist, "utf-8");
264
- console.log(`Wrote plist: ${plistPath}`);
265
-
266
- const load = execSafe(`launchctl load "${plistPath}"`);
267
- if (!load.ok) {
268
- console.error(`launchctl load failed: ${load.stderr}`);
269
- process.exit(1);
270
- }
271
-
272
- console.log(`\n✓ Installed and started launchd agent: ${LAUNCHD_LABEL}`);
273
- console.log(` Logs: tail -f ${join(logDir, "daemon.log")}`);
274
- }
275
-
276
- async function uninstallLaunchd(): Promise<void> {
277
- const plistPath = launchdPlistPath();
278
-
279
- if (existsSync(plistPath)) {
280
- execSafe(`launchctl unload "${plistPath}"`);
281
- unlinkSync(plistPath);
282
- console.log(`Removed: ${plistPath}`);
283
- } else {
284
- console.log(`Plist not found at ${plistPath}`);
285
- }
286
-
287
- console.log(`✓ Uninstalled launchd agent.`);
288
- }
289
-
290
- function logsLaunchd(): void {
291
- const logFile = join(getAppDir(), "logs", "daemon.log");
292
- console.log(`Log file: ${logFile}\n`);
293
-
294
- if (existsSync(logFile)) {
295
- const content = readFileSync(logFile, "utf-8");
296
- const lines = content.split("\n");
297
- const last50 = lines.slice(-50).join("\n");
298
- console.log(last50);
299
- } else {
300
- console.log("No logs found.");
301
- }
302
- }
303
-
304
- function statusLaunchd(): void {
305
- const result = execSafe(`launchctl list | grep ${LAUNCHD_LABEL}`);
306
- if (result.ok && result.stdout) {
307
- const parts = result.stdout.split(/\s+/);
308
- const pid = parts[0];
309
- const exitCode = parts[1];
310
- if (pid && pid !== "-") {
311
- console.log(`Daemon is running (pid ${pid}).`);
312
- } else {
313
- console.log(`Daemon is not running (last exit code: ${exitCode}).`);
314
- }
315
- } else {
316
- console.log("Daemon is not installed as a launchd agent.");
317
- }
318
- }
319
-
320
- // ─── Platform dispatch ────────────────────────────────────────────────────────
321
-
322
- const isMac = platform() === "darwin";
323
- const isLinux = platform() === "linux";
324
-
325
- export async function installService(): Promise<void> {
326
- if (isMac) {
327
- await installLaunchd();
328
- } else if (isLinux) {
329
- await installSystemd();
330
- } else {
331
- console.error(`Service installation not supported on ${platform()}.`);
332
- console.log("Run 'clankie start' manually instead.");
333
- process.exit(1);
334
- }
335
- }
336
-
337
- export async function uninstallService(): Promise<void> {
338
- if (isMac) {
339
- await uninstallLaunchd();
340
- } else if (isLinux) {
341
- await uninstallSystemd();
342
- } else {
343
- console.error(`Service management not supported on ${platform()}.`);
344
- process.exit(1);
345
- }
346
- }
347
-
348
- export function showServiceLogs(): void {
349
- if (isMac) {
350
- logsLaunchd();
351
- } else if (isLinux) {
352
- logsSystemd();
353
- } else {
354
- const logFile = join(getAppDir(), "logs", "daemon.log");
355
- if (existsSync(logFile)) {
356
- console.log(readFileSync(logFile, "utf-8"));
357
- } else {
358
- console.log("No logs found.");
359
- }
360
- }
361
- }
362
-
363
- export function showServiceStatus(): void {
364
- if (isMac) {
365
- statusLaunchd();
366
- } else if (isLinux) {
367
- statusSystemd();
368
- } else {
369
- console.log(`Service management not supported on ${platform()}.`);
370
- console.log("Use 'clankie status' to check if the daemon is running via PID file.");
371
- }
372
- }
package/src/sessions.ts DELETED
@@ -1,251 +0,0 @@
1
- /**
2
- * Shared session management for channels.
3
- *
4
- * Both daemon (for Slack) and WebChannel (for web-ui) use this
5
- * to create and cache agent sessions per chat.
6
- */
7
-
8
- import { existsSync, mkdirSync } from "node:fs";
9
- import { join } from "node:path";
10
- import type { ImageContent } from "@mariozechner/pi-ai";
11
- import {
12
- type AgentSession,
13
- AuthStorage,
14
- type CreateAgentSessionResult,
15
- createAgentSession,
16
- DefaultResourceLoader,
17
- ModelRegistry,
18
- SessionManager,
19
- } from "@mariozechner/pi-coding-agent";
20
- import type { Attachment } from "./channels/channel.ts";
21
- import { type AppConfig, getAgentDir, getAppDir, getAuthPath, getWorkspace } from "./config.ts";
22
-
23
- // ─── Session cache (one session per chat) ──────────────────────────────────────
24
-
25
- const sessionCache = new Map<string, AgentSession>();
26
-
27
- // Track active session name per chat (for /switch command)
28
- const activeSessionNames = new Map<string, string>();
29
-
30
- /** Lock to serialize message processing per chat */
31
- const chatLocks = new Map<string, Promise<void>>();
32
-
33
- // ─── Session factory ───────────────────────────────────────────────────────────
34
-
35
- export async function getOrCreateSession(chatKey: string, config: AppConfig): Promise<AgentSession> {
36
- console.log(`[session] getOrCreateSession called - chatKey: ${chatKey}, cache has: ${sessionCache.size} entries`);
37
- const cached = sessionCache.get(chatKey);
38
- if (cached) {
39
- console.log(`[session] Returning cached session - chatKey: ${chatKey}, session.sessionId: ${cached.sessionId}`);
40
- return cached;
41
- }
42
- console.log(`[session] No cached session found for chatKey: ${chatKey}, creating new...`);
43
-
44
- const agentDir = getAgentDir(config);
45
- const cwd = getWorkspace(config);
46
-
47
- const authStorage = AuthStorage.create(getAuthPath());
48
- const modelRegistry = new ModelRegistry(authStorage);
49
-
50
- const loader = new DefaultResourceLoader({
51
- cwd,
52
- agentDir,
53
- });
54
- await loader.reload();
55
-
56
- // Use a stable session directory per chat so conversations persist across restarts
57
- const sessionDir = join(getAppDir(), "sessions", chatKey);
58
-
59
- // Ensure session directory exists
60
- if (!existsSync(sessionDir)) {
61
- mkdirSync(sessionDir, { recursive: true });
62
- }
63
-
64
- // SessionManager.continueRecent continues the most recent session in the given directory
65
- // It SHOULD keep using that directory for all future saves
66
- const sessionManager = SessionManager.continueRecent(cwd, sessionDir);
67
- console.log(`[session] SessionManager created for chatKey: ${chatKey}, sessionDir: ${sessionDir}`);
68
-
69
- // Resolve model from config → pi auto-detection
70
- const modelSpec = config.agent?.model?.primary;
71
- let model: ReturnType<typeof modelRegistry.find> | undefined;
72
- if (modelSpec) {
73
- const slash = modelSpec.indexOf("/");
74
- if (slash !== -1) {
75
- const provider = modelSpec.substring(0, slash);
76
- const modelId = modelSpec.substring(slash + 1);
77
- model = modelRegistry.find(provider, modelId);
78
- if (!model) {
79
- console.warn(`[session] Warning: model "${modelSpec}" from config not found, falling back to auto-detection`);
80
- }
81
- }
82
- }
83
-
84
- const result: CreateAgentSessionResult = await createAgentSession({
85
- cwd,
86
- agentDir,
87
- authStorage,
88
- modelRegistry,
89
- resourceLoader: loader,
90
- sessionManager,
91
- model,
92
- });
93
-
94
- const { session } = result;
95
- console.log(
96
- `[session] Created AgentSession - chatKey: ${chatKey}, session.sessionId: ${session.sessionId}, session.sessionFile: ${session.sessionFile}`,
97
- );
98
-
99
- // Bind extensions (headless — no UI)
100
- await session.bindExtensions({
101
- commandContextActions: {
102
- waitForIdle: () => session.agent.waitForIdle(),
103
- newSession: async (opts) => {
104
- const success = await session.newSession({ parentSession: opts?.parentSession });
105
- if (success && opts?.setup) {
106
- await opts.setup(session.sessionManager);
107
- }
108
- return { cancelled: !success };
109
- },
110
- fork: async (entryId) => {
111
- const r = await session.fork(entryId);
112
- return { cancelled: r.cancelled };
113
- },
114
- navigateTree: async (targetId, opts) => {
115
- const r = await session.navigateTree(targetId, {
116
- summarize: opts?.summarize,
117
- customInstructions: opts?.customInstructions,
118
- replaceInstructions: opts?.replaceInstructions,
119
- label: opts?.label,
120
- });
121
- return { cancelled: r.cancelled };
122
- },
123
- switchSession: async (sessionPath) => {
124
- const success = await session.switchSession(sessionPath);
125
- return { cancelled: !success };
126
- },
127
- reload: async () => {
128
- await session.reload();
129
- },
130
- },
131
- onError: (err) => {
132
- console.error(`[session] Extension error (${err.extensionPath}): ${err.error}`);
133
- },
134
- });
135
-
136
- // Subscribe to enable session persistence
137
- session.subscribe(() => {});
138
-
139
- console.log(`[session] Caching session - chatKey: ${chatKey}, session.sessionId: ${session.sessionId}`);
140
- sessionCache.set(chatKey, session);
141
-
142
- // Log the cache state
143
- console.log(`[session] Session cache now has ${sessionCache.size} entries`);
144
-
145
- return session;
146
- }
147
-
148
- // ─── Session helpers ───────────────────────────────────────────────────────────
149
-
150
- /**
151
- * List all session names for a given chat identifier.
152
- * Scans ~/.clankie/sessions/ for directories matching the chatIdentifier prefix.
153
- */
154
- export function listSessionNames(chatIdentifier: string): string[] {
155
- const sessionsDir = join(getAppDir(), "sessions");
156
- if (!existsSync(sessionsDir)) {
157
- return [];
158
- }
159
-
160
- try {
161
- const { readdirSync, statSync } = require("node:fs");
162
- const entries = readdirSync(sessionsDir);
163
- const sessionNames = new Set<string>();
164
-
165
- for (const entry of entries) {
166
- // Session directories are named: {channel}_{chatId}_{sessionName}
167
- // We want to extract unique sessionNames for this chatIdentifier
168
- if (entry.startsWith(`${chatIdentifier}_`)) {
169
- const entryPath = join(sessionsDir, entry);
170
- if (statSync(entryPath).isDirectory()) {
171
- // Extract session name from: chatIdentifier_sessionName
172
- const sessionName = entry.substring(chatIdentifier.length + 1);
173
- if (sessionName) {
174
- sessionNames.add(sessionName);
175
- }
176
- }
177
- }
178
- }
179
-
180
- return Array.from(sessionNames).sort();
181
- } catch (err) {
182
- console.error(`[session] Error listing session names: ${err instanceof Error ? err.message : String(err)}`);
183
- return [];
184
- }
185
- }
186
-
187
- /**
188
- * Get or set active session name for a chat.
189
- */
190
- export function getActiveSessionName(chatIdentifier: string): string {
191
- return activeSessionNames.get(chatIdentifier) ?? "default";
192
- }
193
-
194
- export function setActiveSessionName(chatIdentifier: string, sessionName: string): void {
195
- activeSessionNames.set(chatIdentifier, sessionName);
196
- }
197
-
198
- // ─── Attachment helpers ────────────────────────────────────────────────────────
199
-
200
- const IMAGE_MIME_PREFIXES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
201
-
202
- /** Convert image attachments to pi's ImageContent format for vision models. */
203
- export function toImageContents(attachments?: Attachment[]): ImageContent[] {
204
- if (!attachments) return [];
205
- return attachments
206
- .filter((a) => IMAGE_MIME_PREFIXES.some((prefix) => a.mimeType.startsWith(prefix)))
207
- .map((a) => ({ type: "image" as const, data: a.data, mimeType: a.mimeType }));
208
- }
209
-
210
- /** Save non-image attachments to disk and return their paths. */
211
- export async function saveNonImageAttachments(
212
- attachments: Attachment[] | undefined,
213
- chatKey: string,
214
- ): Promise<{ fileName: string; path: string }[]> {
215
- if (!attachments) return [];
216
-
217
- const nonImages = attachments.filter((a) => !IMAGE_MIME_PREFIXES.some((prefix) => a.mimeType.startsWith(prefix)));
218
- if (nonImages.length === 0) return [];
219
-
220
- const { mkdirSync, writeFileSync } = await import("node:fs");
221
- const { join } = await import("node:path");
222
-
223
- const dir = join(getAppDir(), "attachments", chatKey);
224
- mkdirSync(dir, { recursive: true });
225
-
226
- const results: { fileName: string; path: string }[] = [];
227
- for (const att of nonImages) {
228
- const name = att.fileName || `file_${Date.now()}`;
229
- const filePath = join(dir, name);
230
- writeFileSync(filePath, Buffer.from(att.data, "base64"));
231
- results.push({ fileName: name, path: filePath });
232
- console.log(`[session] Saved attachment: ${filePath} (${att.mimeType})`);
233
- }
234
- return results;
235
- }
236
-
237
- // ─── Chat lock helpers ─────────────────────────────────────────────────────────
238
-
239
- /**
240
- * Acquire a lock for a chat to serialize message processing.
241
- * Returns a promise that completes when the action finishes.
242
- */
243
- export async function withChatLock<T>(chatKey: string, action: () => Promise<T>): Promise<T> {
244
- const previous = chatLocks.get(chatKey) ?? Promise.resolve();
245
- const current = previous.then(action, action); // Run action even if previous failed
246
- chatLocks.set(
247
- chatKey,
248
- current.catch(() => {}),
249
- ); // Swallow errors in the chain
250
- return current;
251
- }
@@ -1 +0,0 @@
1
- import{c as w,S as T,m as u,q,w as z,r as x,p as L,j as e,L as B,B as p,A as R,P as V,y as o}from"./main-cBOaKYCP.js";import{b as j,c as g,d as f,e as N,f as v,F as $,a as G,I as H,C as J}from"./card-kSKmECr1.js";import{L as E}from"./loader-circle-C5ib508E.js";const K=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],y=w("circle-alert",K);const O=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",key:"1a0edw"}],["path",{d:"M12 22V12",key:"d0xqtd"}],["polyline",{points:"3.29 7 12 12 20.71 7",key:"ousv84"}],["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}]],P=w("package",O);const Q=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",key:"1s2grr"}],["path",{d:"M20 2v4",key:"1rf3ol"}],["path",{d:"M22 4h-4",key:"gwowj6"}],["circle",{cx:"4",cy:"20",r:"2",key:"6kqj1y"}]],U=w("sparkles",Q),W={extensions:[],extensionErrors:[],skills:[],skillDiagnostics:[],isLoading:!1,installStatus:{isInstalling:!1,output:"",exitCode:null}},r=new T(W);function F(i){r.setState(a=>({...a,isLoading:i}))}function X(i,a){r.setState(c=>({...c,extensions:i,extensionErrors:a,isLoading:!1}))}function Y(i,a){r.setState(c=>({...c,skills:i,skillDiagnostics:a,isLoading:!1}))}function k(i){r.setState(a=>({...a,installStatus:{...a.installStatus,...i}}))}function Z(){r.setState(i=>({...i,installStatus:{isInstalling:!1,output:"",exitCode:null,error:void 0}}))}function ae(){const{status:i}=u(q,s=>({status:s.status})),{activeSessionId:a}=u(z,s=>({activeSessionId:s.activeSessionId})),{extensions:c,extensionErrors:S,skills:b,skillDiagnostics:C,isLoading:A,installStatus:n}=u(r,s=>s),[d,M]=x.useState(""),[I,_]=x.useState(!1),m=i==="connected",h=x.useCallback(async()=>{const s=L.getClient();if(!(!s||!a)){F(!0);try{const[t,l]=await Promise.all([s.getExtensions(a),s.getSkills(a)]);X(t.extensions,t.errors),Y(l.skills,l.diagnostics)}catch(t){console.error("Failed to load extensions and skills:",t),F(!1)}}},[a]);x.useEffect(()=>{m&&a&&h()},[m,a,h]);const D=async()=>{const s=L.getClient();if(!(!s||!a||!d.trim())){k({isInstalling:!0,output:"",exitCode:null,error:void 0});try{const t=await s.installPackage(a,d.trim(),I);k({isInstalling:!1,output:t.output,exitCode:t.exitCode}),t.exitCode===0&&await h()}catch(t){k({isInstalling:!1,error:t instanceof Error?t.message:String(t)})}}};return m?A?e.jsx("div",{className:"flex h-full items-center justify-center",children:e.jsxs("div",{className:"text-center space-y-2",children:[e.jsx(E,{className:"inline-block h-8 w-8 animate-spin text-primary"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Loading extensions and skills..."})]})}):e.jsx("div",{className:"h-full overflow-y-auto",children:e.jsxs("div",{className:"container max-w-4xl py-8 px-4 space-y-6",children:[e.jsxs(j,{children:[e.jsxs(g,{children:[e.jsxs(f,{className:"flex items-center gap-2",children:[e.jsx(V,{className:"h-5 w-5"}),"Extensions"]}),e.jsx(N,{children:"Loaded extensions with their registered tools and commands"})]}),e.jsxs(v,{children:[S.length>0&&e.jsx("div",{className:"mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsx(y,{className:"h-4 w-4 text-destructive mt-0.5"}),e.jsxs("div",{className:"flex-1",children:[e.jsx("p",{className:"text-sm font-medium text-destructive",children:"Extension Load Errors"}),e.jsx("div",{className:"mt-2 space-y-2",children:S.map((s,t)=>e.jsxs("div",{className:"text-xs",children:[e.jsx("p",{className:"font-mono text-muted-foreground",children:s.path}),e.jsx("p",{className:"text-destructive",children:s.error})]},t))})]})]})}),c.length===0?e.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"No extensions loaded."}):e.jsx("div",{className:"space-y-3",children:c.map((s,t)=>e.jsx("div",{className:"rounded-lg border p-3",children:e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-medium font-mono break-all",children:s.path}),s.resolvedPath!==s.path&&e.jsxs("p",{className:"text-xs text-muted-foreground font-mono mt-1 break-all",children:["→ ",s.resolvedPath]})]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[s.tools.length>0&&e.jsxs("div",{className:"flex flex-wrap gap-1",children:[e.jsx("span",{className:"text-xs text-muted-foreground mr-1",children:"Tools:"}),s.tools.map(l=>e.jsx(o,{variant:"secondary",className:"text-xs",children:l},l))]}),s.commands.length>0&&e.jsxs("div",{className:"flex flex-wrap gap-1",children:[e.jsx("span",{className:"text-xs text-muted-foreground mr-1",children:"Commands:"}),s.commands.map(l=>e.jsxs(o,{variant:"default",className:"text-xs",children:["/",l]},l))]}),s.flags.length>0&&e.jsxs("div",{className:"flex flex-wrap gap-1",children:[e.jsx("span",{className:"text-xs text-muted-foreground mr-1",children:"Flags:"}),s.flags.map(l=>e.jsxs(o,{variant:"outline",className:"text-xs",children:["--",l]},l))]}),s.shortcuts.length>0&&e.jsxs("div",{className:"flex flex-wrap gap-1",children:[e.jsx("span",{className:"text-xs text-muted-foreground mr-1",children:"Shortcuts:"}),s.shortcuts.map(l=>e.jsx(o,{variant:"outline",className:"text-xs font-mono",children:l},l))]})]})]})},t))})]})]}),e.jsxs(j,{children:[e.jsxs(g,{children:[e.jsxs(f,{className:"flex items-center gap-2",children:[e.jsx(U,{className:"h-5 w-5"}),"Skills"]}),e.jsx(N,{children:"Available skills for the agent"})]}),e.jsxs(v,{children:[C.length>0&&e.jsx("div",{className:"mb-4 rounded-md border border-yellow-500/50 bg-yellow-500/10 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsx(y,{className:"h-4 w-4 text-yellow-600 mt-0.5"}),e.jsxs("div",{className:"flex-1",children:[e.jsx("p",{className:"text-sm font-medium text-yellow-600",children:"Skill Diagnostics"}),e.jsx("div",{className:"mt-2 space-y-1",children:C.map((s,t)=>e.jsxs("p",{className:"text-xs text-yellow-700",children:[s.path&&e.jsxs("span",{className:"font-mono",children:[s.path,": "]}),s.message]},t))})]})]})}),b.length===0?e.jsx("p",{className:"text-sm text-muted-foreground py-4",children:"No skills loaded."}):e.jsx("div",{className:"space-y-3",children:b.map((s,t)=>e.jsx("div",{className:"rounded-lg border p-3",children:e.jsxs("div",{className:"space-y-2",children:[e.jsx("div",{className:"flex items-start justify-between gap-2",children:e.jsxs("div",{className:"flex-1",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("p",{className:"text-sm font-medium",children:s.name}),s.disableModelInvocation&&e.jsx(o,{variant:"outline",className:"text-xs",children:"Manual only"})]}),e.jsx("p",{className:"text-xs text-muted-foreground mt-1",children:s.description})]})}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs text-muted-foreground",children:[e.jsxs("span",{children:[e.jsx("span",{className:"font-medium",children:"Source:"})," ",s.source]}),e.jsx("span",{children:"•"}),e.jsx("span",{className:"font-mono break-all",children:s.filePath})]})]})},t))})]})]}),e.jsxs(j,{children:[e.jsxs(g,{children:[e.jsxs(f,{className:"flex items-center gap-2",children:[e.jsx(P,{className:"h-5 w-5"}),"Install Package"]}),e.jsx(N,{children:"Install a Pi package from npm, git, or a local path"})]}),e.jsxs(v,{className:"space-y-4",children:[e.jsxs($,{children:[e.jsx(G,{htmlFor:"package-source",children:"Package Source"}),e.jsx(H,{id:"package-source",type:"text",placeholder:"npm:@foo/bar@1.0.0, git:github.com/user/repo, or /path/to/package",value:d,onChange:s=>M(s.target.value),disabled:n.isInstalling}),e.jsxs("p",{className:"text-xs text-muted-foreground mt-1",children:["Examples: ",e.jsx("code",{className:"text-xs",children:"npm:package-name"}),","," ",e.jsx("code",{className:"text-xs",children:"git:github.com/user/repo"}),","," ",e.jsx("code",{className:"text-xs",children:"/absolute/path"})]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("input",{type:"checkbox",id:"install-local",checked:I,onChange:s=>_(s.target.checked),disabled:n.isInstalling,className:"h-4 w-4"}),e.jsx("label",{htmlFor:"install-local",className:"text-sm cursor-pointer",children:"Install to project settings (.pi/settings.json) instead of global"})]}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(p,{onClick:D,disabled:n.isInstalling||!d.trim(),className:"flex-1",children:n.isInstalling?e.jsxs(e.Fragment,{children:[e.jsx(E,{className:"mr-2 h-4 w-4 animate-spin"}),"Installing..."]}):e.jsxs(e.Fragment,{children:[e.jsx(P,{className:"mr-2 h-4 w-4"}),"Install"]})}),(n.output||n.error)&&e.jsx(p,{variant:"outline",onClick:Z,children:"Clear"})]}),n.error&&e.jsxs("div",{className:"rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive",children:[e.jsx("p",{className:"font-medium",children:"Error"}),e.jsx("p",{className:"text-xs mt-1",children:n.error})]}),n.output&&e.jsxs("div",{className:"space-y-2",children:[e.jsx("div",{className:"flex items-center gap-2",children:n.exitCode===0?e.jsxs(e.Fragment,{children:[e.jsx(J,{className:"h-4 w-4 text-green-600"}),e.jsx("p",{className:"text-sm font-medium text-green-600",children:"Installation Successful"})]}):e.jsxs(e.Fragment,{children:[e.jsx(y,{className:"h-4 w-4 text-destructive"}),e.jsxs("p",{className:"text-sm font-medium text-destructive",children:["Installation Failed (exit code: ",n.exitCode,")"]})]})}),e.jsx("div",{className:"rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap break-all max-h-60 overflow-y-auto",children:n.output})]})]})]})]})}):e.jsx("div",{className:"flex h-full items-center justify-center",children:e.jsxs("div",{className:"text-center space-y-4",children:[e.jsxs("div",{className:"space-y-2",children:[e.jsx("h2",{className:"text-2xl font-semibold",children:"Not Connected"}),e.jsx("p",{className:"text-muted-foreground",children:"Configure your connection to get started"})]}),e.jsx(B,{to:"/settings",children:e.jsxs(p,{children:[e.jsx(R,{className:"mr-2 h-4 w-4"}),"Go to Settings"]})})]})})}export{ae as component};