wispy-cli 0.6.1 → 0.8.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/bin/wispy.mjs +172 -2
- package/core/config.mjs +104 -0
- package/core/cron.mjs +346 -0
- package/core/engine.mjs +705 -0
- package/core/index.mjs +14 -0
- package/core/mcp.mjs +8 -0
- package/core/memory.mjs +275 -0
- package/core/providers.mjs +410 -0
- package/core/session.mjs +196 -0
- package/core/tools.mjs +526 -0
- package/lib/channels/index.mjs +45 -246
- package/lib/wispy-repl.mjs +396 -2452
- package/lib/wispy-tui.mjs +105 -588
- package/package.json +7 -4
package/core/session.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/session.mjs — Session management for Wispy
|
|
3
|
+
*
|
|
4
|
+
* Class SessionManager:
|
|
5
|
+
* - create({ workstream?, channel?, chatId? }) → Session
|
|
6
|
+
* - get(id) → Session
|
|
7
|
+
* - list(filter?) → Session[]
|
|
8
|
+
* - addMessage(id, message) → void
|
|
9
|
+
* - clear(id) → void
|
|
10
|
+
* - save(id) → void
|
|
11
|
+
* - load(id) → Session
|
|
12
|
+
* - getOrCreate(key) → Session (for channel adapters: key = "telegram:chatId")
|
|
13
|
+
*
|
|
14
|
+
* File-based persistence: ~/.wispy/sessions/{id}.json
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
|
|
20
|
+
import { SESSIONS_DIR } from "./config.mjs";
|
|
21
|
+
|
|
22
|
+
export class Session {
|
|
23
|
+
constructor({ id, workstream = "default", channel = null, chatId = null, messages = [], createdAt = null }) {
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.workstream = workstream;
|
|
26
|
+
this.channel = channel;
|
|
27
|
+
this.chatId = chatId;
|
|
28
|
+
this.messages = messages;
|
|
29
|
+
this.createdAt = createdAt ?? new Date().toISOString();
|
|
30
|
+
this.updatedAt = new Date().toISOString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toJSON() {
|
|
34
|
+
return {
|
|
35
|
+
id: this.id,
|
|
36
|
+
workstream: this.workstream,
|
|
37
|
+
channel: this.channel,
|
|
38
|
+
chatId: this.chatId,
|
|
39
|
+
messages: this.messages,
|
|
40
|
+
createdAt: this.createdAt,
|
|
41
|
+
updatedAt: this.updatedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class SessionManager {
|
|
47
|
+
constructor() {
|
|
48
|
+
this._sessions = new Map(); // id → Session (in-memory cache)
|
|
49
|
+
this._keyMap = new Map(); // composite key → id (for getOrCreate)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a new session
|
|
54
|
+
*/
|
|
55
|
+
create({ workstream = "default", channel = null, chatId = null } = {}) {
|
|
56
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
57
|
+
const session = new Session({ id, workstream, channel, chatId });
|
|
58
|
+
this._sessions.set(id, session);
|
|
59
|
+
if (channel && chatId) {
|
|
60
|
+
this._keyMap.set(`${channel}:${chatId}`, id);
|
|
61
|
+
}
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a session by ID. Returns null if not found in memory.
|
|
67
|
+
*/
|
|
68
|
+
get(id) {
|
|
69
|
+
return this._sessions.get(id) ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get or load a session by ID.
|
|
74
|
+
*/
|
|
75
|
+
async getOrLoad(id) {
|
|
76
|
+
if (this._sessions.has(id)) return this._sessions.get(id);
|
|
77
|
+
return this.load(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List sessions matching an optional filter.
|
|
82
|
+
*/
|
|
83
|
+
list(filter = null) {
|
|
84
|
+
const all = Array.from(this._sessions.values());
|
|
85
|
+
if (!filter) return all;
|
|
86
|
+
return all.filter(s => {
|
|
87
|
+
if (filter.workstream && s.workstream !== filter.workstream) return false;
|
|
88
|
+
if (filter.channel && s.channel !== filter.channel) return false;
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Add a message to a session.
|
|
95
|
+
*/
|
|
96
|
+
addMessage(id, message) {
|
|
97
|
+
const session = this._sessions.get(id);
|
|
98
|
+
if (!session) throw new Error(`Session not found: ${id}`);
|
|
99
|
+
session.messages.push(message);
|
|
100
|
+
session.updatedAt = new Date().toISOString();
|
|
101
|
+
// Keep last 50 messages
|
|
102
|
+
if (session.messages.length > 50) {
|
|
103
|
+
session.messages = session.messages.slice(-50);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear message history for a session.
|
|
109
|
+
*/
|
|
110
|
+
clear(id) {
|
|
111
|
+
const session = this._sessions.get(id);
|
|
112
|
+
if (!session) throw new Error(`Session not found: ${id}`);
|
|
113
|
+
session.messages = [];
|
|
114
|
+
session.updatedAt = new Date().toISOString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Save a session to disk.
|
|
119
|
+
*/
|
|
120
|
+
async save(id) {
|
|
121
|
+
const session = this._sessions.get(id);
|
|
122
|
+
if (!session) throw new Error(`Session not found: ${id}`);
|
|
123
|
+
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
124
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
125
|
+
await writeFile(filePath, JSON.stringify(session.toJSON(), null, 2) + "\n", "utf8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load a session from disk.
|
|
130
|
+
*/
|
|
131
|
+
async load(id) {
|
|
132
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
133
|
+
try {
|
|
134
|
+
const raw = await readFile(filePath, "utf8");
|
|
135
|
+
const data = JSON.parse(raw);
|
|
136
|
+
const session = new Session(data);
|
|
137
|
+
this._sessions.set(id, session);
|
|
138
|
+
if (session.channel && session.chatId) {
|
|
139
|
+
this._keyMap.set(`${session.channel}:${session.chatId}`, id);
|
|
140
|
+
}
|
|
141
|
+
return session;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get or create a session by a composite key (e.g., "telegram:chatId").
|
|
149
|
+
* Used by channel adapters to maintain per-chat sessions.
|
|
150
|
+
*/
|
|
151
|
+
async getOrCreate(key, opts = {}) {
|
|
152
|
+
// Check in-memory key map
|
|
153
|
+
if (this._keyMap.has(key)) {
|
|
154
|
+
const id = this._keyMap.get(key);
|
|
155
|
+
if (this._sessions.has(id)) return this._sessions.get(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Try to find on disk by scanning sessions dir
|
|
159
|
+
try {
|
|
160
|
+
const files = await readdir(SESSIONS_DIR);
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (!file.endsWith(".json")) continue;
|
|
163
|
+
const id = file.replace(".json", "");
|
|
164
|
+
const existing = await this.load(id);
|
|
165
|
+
if (existing) {
|
|
166
|
+
const existingKey = existing.channel && existing.chatId
|
|
167
|
+
? `${existing.channel}:${existing.chatId}`
|
|
168
|
+
: null;
|
|
169
|
+
if (existingKey === key) return existing;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch { /* sessions dir might not exist yet */ }
|
|
173
|
+
|
|
174
|
+
// Parse channel:chatId from key
|
|
175
|
+
const colonIdx = key.indexOf(":");
|
|
176
|
+
const channel = colonIdx !== -1 ? key.slice(0, colonIdx) : null;
|
|
177
|
+
const chatId = colonIdx !== -1 ? key.slice(colonIdx + 1) : key;
|
|
178
|
+
|
|
179
|
+
const session = this.create({ channel, chatId, ...opts });
|
|
180
|
+
this._keyMap.set(key, session.id);
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get all messages for a session, including a system prompt prefix.
|
|
186
|
+
* Ensures the system message is always first.
|
|
187
|
+
*/
|
|
188
|
+
getMessages(id) {
|
|
189
|
+
const session = this._sessions.get(id);
|
|
190
|
+
if (!session) return [];
|
|
191
|
+
return session.messages;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Default global instance
|
|
196
|
+
export const sessionManager = new SessionManager();
|