@geminixiang/mama 0.1.9 → 0.2.0-beta.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 +173 -16
- package/dist/adapter.d.ts +8 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +1 -0
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +12 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +86 -10
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +48 -28
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js +4 -2
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +6 -3
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +115 -63
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +2 -2
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +92 -65
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +214 -39
- package/dist/agent.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -13
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +15 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +29 -2
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +10 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +44 -10
- package/dist/events.js.map +1 -1
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +7 -0
- package/dist/instrument.js.map +1 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +6 -5
- package/dist/log.js.map +1 -1
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +137 -59
- package/dist/main.js.map +1 -1
- package/dist/sandbox.d.ts +7 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +127 -27
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +205 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-store.d.ts +76 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +189 -0
- package/dist/session-store.js.map +1 -0
- package/package.json +13 -12
package/dist/sentry.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/node";
|
|
2
|
+
const REDACTED = "[REDACTED]";
|
|
3
|
+
const REDACTED_PATH = "[REDACTED_PATH]";
|
|
4
|
+
const MAX_STRING_LENGTH = 256;
|
|
5
|
+
const MAX_DEPTH = 4;
|
|
6
|
+
const SENSITIVE_KEYS = new Set([
|
|
7
|
+
"args",
|
|
8
|
+
"attachment",
|
|
9
|
+
"attachments",
|
|
10
|
+
"authorization",
|
|
11
|
+
"body",
|
|
12
|
+
"content",
|
|
13
|
+
"contents",
|
|
14
|
+
"cookie",
|
|
15
|
+
"cookies",
|
|
16
|
+
"filePath",
|
|
17
|
+
"headers",
|
|
18
|
+
"image",
|
|
19
|
+
"imageAttachments",
|
|
20
|
+
"images",
|
|
21
|
+
"localPath",
|
|
22
|
+
"messages",
|
|
23
|
+
"newUserMessage",
|
|
24
|
+
"path",
|
|
25
|
+
"paths",
|
|
26
|
+
"prompt",
|
|
27
|
+
"response",
|
|
28
|
+
"result",
|
|
29
|
+
"systemPrompt",
|
|
30
|
+
"text",
|
|
31
|
+
"thinking",
|
|
32
|
+
]);
|
|
33
|
+
const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^\s"'`]+|\/workspace\/[^\s"'`]+|\/tmp\/[^\s"'`]+|\/var\/folders\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/;
|
|
34
|
+
const TOKEN_PATTERNS = [
|
|
35
|
+
/\bsk-[A-Za-z0-9_-]{12,}\b/,
|
|
36
|
+
/\bxox[a-z]-[A-Za-z0-9-]{10,}\b/,
|
|
37
|
+
/\bAIza[0-9A-Za-z_-]{20,}\b/,
|
|
38
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/,
|
|
39
|
+
];
|
|
40
|
+
export function createSentryInitOptions(dsn) {
|
|
41
|
+
return {
|
|
42
|
+
dsn,
|
|
43
|
+
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
|
|
44
|
+
enabled: Boolean(dsn) && process.env.SENTRY_ENABLED !== "false",
|
|
45
|
+
sendDefaultPii: false,
|
|
46
|
+
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 1.0,
|
|
47
|
+
includeLocalVariables: false,
|
|
48
|
+
enableLogs: true,
|
|
49
|
+
beforeSend(event, hint) {
|
|
50
|
+
return sanitizeEvent(event, hint);
|
|
51
|
+
},
|
|
52
|
+
beforeBreadcrumb(breadcrumb) {
|
|
53
|
+
return sanitizeBreadcrumb(breadcrumb);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function applyRunScope(scope, context) {
|
|
58
|
+
scope.setTag("channel_id", context.channelId);
|
|
59
|
+
scope.setTag("session_key", context.sessionKey);
|
|
60
|
+
scope.setTag("platform", context.platform);
|
|
61
|
+
scope.setTag("is_event", String(Boolean(context.isEvent)));
|
|
62
|
+
if (context.threadTs)
|
|
63
|
+
scope.setTag("thread_ts", context.threadTs);
|
|
64
|
+
if (context.provider)
|
|
65
|
+
scope.setTag("provider", context.provider);
|
|
66
|
+
if (context.model)
|
|
67
|
+
scope.setTag("model", context.model);
|
|
68
|
+
scope.setUser({
|
|
69
|
+
id: context.userId,
|
|
70
|
+
username: context.userName,
|
|
71
|
+
});
|
|
72
|
+
scope.setContext("agent_run", {
|
|
73
|
+
channelId: context.channelId,
|
|
74
|
+
sessionKey: context.sessionKey,
|
|
75
|
+
messageId: context.messageId,
|
|
76
|
+
threadTs: context.threadTs,
|
|
77
|
+
platform: context.platform,
|
|
78
|
+
provider: context.provider,
|
|
79
|
+
model: context.model,
|
|
80
|
+
isEvent: Boolean(context.isEvent),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function metricAttributes(attributes) {
|
|
84
|
+
return Object.fromEntries(Object.entries(attributes).filter((entry) => {
|
|
85
|
+
const [, value] = entry;
|
|
86
|
+
return value !== undefined;
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
export function addLifecycleBreadcrumb(message, data) {
|
|
90
|
+
Sentry.addBreadcrumb({
|
|
91
|
+
category: "agent.lifecycle",
|
|
92
|
+
message,
|
|
93
|
+
level: "info",
|
|
94
|
+
data: data ? metricAttributes(data) : undefined,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function sanitizeEvent(event, _hint) {
|
|
98
|
+
const sanitized = {
|
|
99
|
+
...event,
|
|
100
|
+
breadcrumbs: event.breadcrumbs
|
|
101
|
+
?.map((breadcrumb) => sanitizeBreadcrumb(breadcrumb))
|
|
102
|
+
.filter((breadcrumb) => breadcrumb !== null),
|
|
103
|
+
extra: sanitizeValue(event.extra),
|
|
104
|
+
contexts: sanitizeValue(event.contexts),
|
|
105
|
+
request: sanitizeRequest(event.request),
|
|
106
|
+
user: undefined,
|
|
107
|
+
server_name: undefined,
|
|
108
|
+
};
|
|
109
|
+
if (sanitized.message) {
|
|
110
|
+
sanitized.message = sanitizeString(sanitized.message);
|
|
111
|
+
}
|
|
112
|
+
if (sanitized.logentry) {
|
|
113
|
+
sanitized.logentry = {
|
|
114
|
+
...sanitized.logentry,
|
|
115
|
+
message: sanitized.logentry.message ? sanitizeString(sanitized.logentry.message) : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (sanitized.exception?.values) {
|
|
119
|
+
sanitized.exception.values = sanitized.exception.values.map((value) => ({
|
|
120
|
+
...value,
|
|
121
|
+
value: value.value ? sanitizeString(value.value) : value.value,
|
|
122
|
+
stacktrace: value.stacktrace
|
|
123
|
+
? {
|
|
124
|
+
...value.stacktrace,
|
|
125
|
+
frames: value.stacktrace.frames?.map((frame) => ({
|
|
126
|
+
...frame,
|
|
127
|
+
filename: frame.filename ? sanitizeString(frame.filename) : frame.filename,
|
|
128
|
+
abs_path: frame.abs_path ? sanitizeString(frame.abs_path) : frame.abs_path,
|
|
129
|
+
vars: undefined,
|
|
130
|
+
})),
|
|
131
|
+
}
|
|
132
|
+
: value.stacktrace,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
return sanitized;
|
|
136
|
+
}
|
|
137
|
+
export function sanitizeBreadcrumb(breadcrumb) {
|
|
138
|
+
if (breadcrumb.category === "console") {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...breadcrumb,
|
|
143
|
+
message: breadcrumb.message ? sanitizeString(breadcrumb.message) : breadcrumb.message,
|
|
144
|
+
data: sanitizeValue(breadcrumb.data),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function sanitizeValue(value, key, depth = 0) {
|
|
148
|
+
if (value == null)
|
|
149
|
+
return value;
|
|
150
|
+
if (depth > MAX_DEPTH)
|
|
151
|
+
return "[Truncated]";
|
|
152
|
+
if (isSensitiveKey(key)) {
|
|
153
|
+
return summarizeValue(value, key);
|
|
154
|
+
}
|
|
155
|
+
if (typeof value === "string") {
|
|
156
|
+
return sanitizeString(value);
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
return value.slice(0, 20).map((entry) => sanitizeValue(entry, key, depth + 1));
|
|
160
|
+
}
|
|
161
|
+
if (typeof value === "object") {
|
|
162
|
+
const entries = Object.entries(value).map(([entryKey, entryValue]) => [entryKey, sanitizeValue(entryValue, entryKey, depth + 1)]);
|
|
163
|
+
return Object.fromEntries(entries);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
function sanitizeRequest(request) {
|
|
168
|
+
if (!request)
|
|
169
|
+
return request;
|
|
170
|
+
return {
|
|
171
|
+
...request,
|
|
172
|
+
data: request.data ? summarizeValue(request.data, "body") : undefined,
|
|
173
|
+
headers: undefined,
|
|
174
|
+
cookies: undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function isSensitiveKey(key) {
|
|
178
|
+
if (!key)
|
|
179
|
+
return false;
|
|
180
|
+
return SENSITIVE_KEYS.has(key);
|
|
181
|
+
}
|
|
182
|
+
function summarizeValue(value, key) {
|
|
183
|
+
const label = key ?? "field";
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
return `[Redacted ${label}; length=${value.length}]`;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
return `[Redacted ${label}; items=${value.length}]`;
|
|
189
|
+
}
|
|
190
|
+
if (value && typeof value === "object") {
|
|
191
|
+
return `[Redacted ${label}; keys=${Object.keys(value).length}]`;
|
|
192
|
+
}
|
|
193
|
+
return `[Redacted ${label}]`;
|
|
194
|
+
}
|
|
195
|
+
function sanitizeString(value) {
|
|
196
|
+
let sanitized = value.replace(new RegExp(ABSOLUTE_PATH_PATTERN, "g"), REDACTED_PATH);
|
|
197
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
198
|
+
sanitized = sanitized.replace(new RegExp(pattern, "g"), REDACTED);
|
|
199
|
+
}
|
|
200
|
+
if (sanitized.length > MAX_STRING_LENGTH) {
|
|
201
|
+
return `${sanitized.slice(0, MAX_STRING_LENGTH)}… [truncated ${sanitized.length - MAX_STRING_LENGTH} chars]`;
|
|
202
|
+
}
|
|
203
|
+
return sanitized;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=sentry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.js","sourceRoot":"","sources":["../src/sentry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Breadcrumb, ErrorEvent, Event, EventHint, Scope } from \"@sentry/node\";\nimport * as Sentry from \"@sentry/node\";\n\nconst REDACTED = \"[REDACTED]\";\nconst REDACTED_PATH = \"[REDACTED_PATH]\";\nconst MAX_STRING_LENGTH = 256;\nconst MAX_DEPTH = 4;\n\nconst SENSITIVE_KEYS = new Set([\n \"args\",\n \"attachment\",\n \"attachments\",\n \"authorization\",\n \"body\",\n \"content\",\n \"contents\",\n \"cookie\",\n \"cookies\",\n \"filePath\",\n \"headers\",\n \"image\",\n \"imageAttachments\",\n \"images\",\n \"localPath\",\n \"messages\",\n \"newUserMessage\",\n \"path\",\n \"paths\",\n \"prompt\",\n \"response\",\n \"result\",\n \"systemPrompt\",\n \"text\",\n \"thinking\",\n]);\n\nconst ABSOLUTE_PATH_PATTERN =\n /(?:\\/Users\\/[^\\s\"'`]+|\\/workspace\\/[^\\s\"'`]+|\\/tmp\\/[^\\s\"'`]+|\\/var\\/folders\\/[^\\s\"'`]+|[A-Za-z]:\\\\[^\\s\"'`]+)/;\nconst TOKEN_PATTERNS = [\n /\\bsk-[A-Za-z0-9_-]{12,}\\b/,\n /\\bxox[a-z]-[A-Za-z0-9-]{10,}\\b/,\n /\\bAIza[0-9A-Za-z_-]{20,}\\b/,\n /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/,\n];\n\nexport interface SentryRunScopeContext {\n channelId: string;\n sessionKey: string;\n messageId: string;\n platform: string;\n userId: string;\n userName?: string;\n threadTs?: string;\n provider?: string;\n model?: string;\n isEvent?: boolean;\n}\n\nexport function createSentryInitOptions(dsn?: string) {\n return {\n dsn,\n environment: process.env.SENTRY_ENVIRONMENT ?? \"production\",\n enabled: Boolean(dsn) && process.env.SENTRY_ENABLED !== \"false\",\n sendDefaultPii: false,\n tracesSampleRate: process.env.NODE_ENV === \"development\" ? 1.0 : 1.0,\n includeLocalVariables: false,\n enableLogs: true,\n beforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null {\n return sanitizeEvent(event, hint);\n },\n beforeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n return sanitizeBreadcrumb(breadcrumb);\n },\n };\n}\n\nexport function applyRunScope(scope: Scope, context: SentryRunScopeContext): void {\n scope.setTag(\"channel_id\", context.channelId);\n scope.setTag(\"session_key\", context.sessionKey);\n scope.setTag(\"platform\", context.platform);\n scope.setTag(\"is_event\", String(Boolean(context.isEvent)));\n if (context.threadTs) scope.setTag(\"thread_ts\", context.threadTs);\n if (context.provider) scope.setTag(\"provider\", context.provider);\n if (context.model) scope.setTag(\"model\", context.model);\n\n scope.setUser({\n id: context.userId,\n username: context.userName,\n });\n scope.setContext(\"agent_run\", {\n channelId: context.channelId,\n sessionKey: context.sessionKey,\n messageId: context.messageId,\n threadTs: context.threadTs,\n platform: context.platform,\n provider: context.provider,\n model: context.model,\n isEvent: Boolean(context.isEvent),\n });\n}\n\nexport function metricAttributes(\n attributes: Record<string, string | number | boolean | undefined>,\n): Record<string, string | number | boolean> {\n return Object.fromEntries(\n Object.entries(attributes).filter((entry): entry is [string, string | number | boolean] => {\n const [, value] = entry;\n return value !== undefined;\n }),\n );\n}\n\nexport function addLifecycleBreadcrumb(\n message: string,\n data?: Record<string, string | number | boolean | undefined>,\n): void {\n Sentry.addBreadcrumb({\n category: \"agent.lifecycle\",\n message,\n level: \"info\",\n data: data ? metricAttributes(data) : undefined,\n });\n}\n\nexport function sanitizeEvent<T extends Event>(event: T, _hint?: EventHint): T | null {\n const sanitized: T = {\n ...event,\n breadcrumbs: event.breadcrumbs\n ?.map((breadcrumb) => sanitizeBreadcrumb(breadcrumb))\n .filter((breadcrumb): breadcrumb is Breadcrumb => breadcrumb !== null),\n extra: sanitizeValue(event.extra) as T[\"extra\"],\n contexts: sanitizeValue(event.contexts) as T[\"contexts\"],\n request: sanitizeRequest(event.request),\n user: undefined,\n server_name: undefined,\n };\n\n if (sanitized.message) {\n sanitized.message = sanitizeString(sanitized.message);\n }\n\n if (sanitized.logentry) {\n sanitized.logentry = {\n ...sanitized.logentry,\n message: sanitized.logentry.message ? sanitizeString(sanitized.logentry.message) : undefined,\n };\n }\n\n if (sanitized.exception?.values) {\n sanitized.exception.values = sanitized.exception.values.map((value) => ({\n ...value,\n value: value.value ? sanitizeString(value.value) : value.value,\n stacktrace: value.stacktrace\n ? {\n ...value.stacktrace,\n frames: value.stacktrace.frames?.map((frame) => ({\n ...frame,\n filename: frame.filename ? sanitizeString(frame.filename) : frame.filename,\n abs_path: frame.abs_path ? sanitizeString(frame.abs_path) : frame.abs_path,\n vars: undefined,\n })),\n }\n : value.stacktrace,\n }));\n }\n\n return sanitized;\n}\n\nexport function sanitizeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n if (breadcrumb.category === \"console\") {\n return null;\n }\n\n return {\n ...breadcrumb,\n message: breadcrumb.message ? sanitizeString(breadcrumb.message) : breadcrumb.message,\n data: sanitizeValue(breadcrumb.data) as Breadcrumb[\"data\"],\n };\n}\n\nexport function sanitizeValue(value: unknown, key?: string, depth = 0): unknown {\n if (value == null) return value;\n if (depth > MAX_DEPTH) return \"[Truncated]\";\n\n if (isSensitiveKey(key)) {\n return summarizeValue(value, key);\n }\n\n if (typeof value === \"string\") {\n return sanitizeString(value);\n }\n\n if (Array.isArray(value)) {\n return value.slice(0, 20).map((entry) => sanitizeValue(entry, key, depth + 1));\n }\n\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>).map(\n ([entryKey, entryValue]) => [entryKey, sanitizeValue(entryValue, entryKey, depth + 1)],\n );\n return Object.fromEntries(entries);\n }\n\n return value;\n}\n\nfunction sanitizeRequest(request: Event[\"request\"]): Event[\"request\"] {\n if (!request) return request;\n\n return {\n ...request,\n data: request.data ? summarizeValue(request.data, \"body\") : undefined,\n headers: undefined,\n cookies: undefined,\n };\n}\n\nfunction isSensitiveKey(key?: string): boolean {\n if (!key) return false;\n return SENSITIVE_KEYS.has(key);\n}\n\nfunction summarizeValue(value: unknown, key?: string): string {\n const label = key ?? \"field\";\n if (typeof value === \"string\") {\n return `[Redacted ${label}; length=${value.length}]`;\n }\n if (Array.isArray(value)) {\n return `[Redacted ${label}; items=${value.length}]`;\n }\n if (value && typeof value === \"object\") {\n return `[Redacted ${label}; keys=${Object.keys(value as Record<string, unknown>).length}]`;\n }\n return `[Redacted ${label}]`;\n}\n\nfunction sanitizeString(value: string): string {\n let sanitized = value.replace(new RegExp(ABSOLUTE_PATH_PATTERN, \"g\"), REDACTED_PATH);\n for (const pattern of TOKEN_PATTERNS) {\n sanitized = sanitized.replace(new RegExp(pattern, \"g\"), REDACTED);\n }\n if (sanitized.length > MAX_STRING_LENGTH) {\n return `${sanitized.slice(0, MAX_STRING_LENGTH)}… [truncated ${sanitized.length - MAX_STRING_LENGTH} chars]`;\n }\n return sanitized;\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the shared session directory for a channel.
|
|
4
|
+
* Channel sessions use a current pointer within this directory.
|
|
5
|
+
* Thread sessions are stored as fixed files within the same directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getSessionDir(channelDir: string, _sessionKey: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the current active session file for a session directory.
|
|
10
|
+
* Reads the "current" pointer file; creates a new session if none exists
|
|
11
|
+
* or the pointed-to file is missing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveSessionFile(sessionDir: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the current active session file for a session directory.
|
|
16
|
+
* Creates a fully initialized persistent session with the provided cwd when none exists.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveManagedSessionFile(sessionDir: string, cwd: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Extracts the short UUID from a session file path.
|
|
21
|
+
* e.g. "2026-04-05T00-00_7b54cf90.jsonl" → "7b54cf90"
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractSessionUuid(sessionFile: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the thread/suffix part of a session key.
|
|
26
|
+
* "channelId:threadId" → "threadId", "channelId" → "channelId"
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractSessionSuffix(sessionKey: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Creates an empty timestamped file and updates the "current" pointer.
|
|
31
|
+
* Used only by tests for placeholder-file scenarios.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createNewSessionFile(sessionDir: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new persistent session file with a proper SessionManager header and cwd.
|
|
36
|
+
* Also updates the "current" pointer.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createManagedSessionFile(sessionDir: string, cwd: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Open a session file with an explicit cwd, even if the file does not exist yet.
|
|
41
|
+
* This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.
|
|
42
|
+
*/
|
|
43
|
+
export declare function openManagedSession(sessionFile: string, sessionDir: string, cwd: string): SessionManager;
|
|
44
|
+
/**
|
|
45
|
+
* Creates or overwrites a fixed-path session file with a valid session header.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Returns the channel-level session directory: {channelDir}/sessions/
|
|
50
|
+
*/
|
|
51
|
+
export declare function getChannelSessionDir(channelDir: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the fixed session file path for a Slack thread.
|
|
54
|
+
*/
|
|
55
|
+
export declare function getThreadSessionFile(channelDir: string, sessionKey: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Try to resolve an existing current session file.
|
|
58
|
+
* Returns null if no current pointer exists or the pointed file has no valid session header.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tryResolveCurrentSession(sessionDir: string): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Try to resolve an existing thread session file.
|
|
63
|
+
* Returns the file path if found, or null if no valid thread session exists yet.
|
|
64
|
+
*/
|
|
65
|
+
export declare function tryResolveThreadSession(sessionFile: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the channel's current session file path (for fork source).
|
|
68
|
+
* Returns null if no channel session exists.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveChannelSessionFile(channelDir: string): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Fork a channel session into a fixed thread-session path.
|
|
73
|
+
* The resulting file keeps forkFrom's distinct session/header metadata.
|
|
74
|
+
*/
|
|
75
|
+
export declare function forkThreadSessionFile(sourceSessionFile: string, targetSessionFile: string, cwd: string): string;
|
|
76
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,cAAc,CAKhB;AAQD;;GAEG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAeD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AA6BD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,GACV,MAAM,CAWR","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@mariozechner/pi-coding-agent\";\n\n/**\n * Returns the shared session directory for a channel.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getSessionDir(channelDir: string, _sessionKey: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n writeFileSync(filePath, \"\", \"utf-8\");\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n writeFileSync(sessionFile, `${JSON.stringify(header)}\\n`, \"utf-8\");\n}\n\n/**\n * Returns the channel-level session directory: {channelDir}/sessions/\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n const channelSessionDir = getChannelSessionDir(channelDir);\n return tryResolveCurrentSession(channelSessionDir);\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n"]}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
/**
|
|
6
|
+
* Returns the shared session directory for a channel.
|
|
7
|
+
* Channel sessions use a current pointer within this directory.
|
|
8
|
+
* Thread sessions are stored as fixed files within the same directory.
|
|
9
|
+
*/
|
|
10
|
+
export function getSessionDir(channelDir, _sessionKey) {
|
|
11
|
+
return join(channelDir, "sessions");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the current active session file for a session directory.
|
|
15
|
+
* Reads the "current" pointer file; creates a new session if none exists
|
|
16
|
+
* or the pointed-to file is missing.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveSessionFile(sessionDir) {
|
|
19
|
+
const existing = tryResolveCurrentSession(sessionDir);
|
|
20
|
+
if (existing)
|
|
21
|
+
return existing;
|
|
22
|
+
return createNewSessionFile(sessionDir);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the current active session file for a session directory.
|
|
26
|
+
* Creates a fully initialized persistent session with the provided cwd when none exists.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveManagedSessionFile(sessionDir, cwd) {
|
|
29
|
+
const existingPath = getCurrentSessionPath(sessionDir);
|
|
30
|
+
if (existingPath)
|
|
31
|
+
return existingPath;
|
|
32
|
+
return createManagedSessionFile(sessionDir, cwd);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extracts the short UUID from a session file path.
|
|
36
|
+
* e.g. "2026-04-05T00-00_7b54cf90.jsonl" → "7b54cf90"
|
|
37
|
+
*/
|
|
38
|
+
export function extractSessionUuid(sessionFile) {
|
|
39
|
+
const base = sessionFile.split("/").pop() ?? sessionFile;
|
|
40
|
+
return base.replace(".jsonl", "").split("_").pop() ?? base;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extracts the thread/suffix part of a session key.
|
|
44
|
+
* "channelId:threadId" → "threadId", "channelId" → "channelId"
|
|
45
|
+
*/
|
|
46
|
+
export function extractSessionSuffix(sessionKey) {
|
|
47
|
+
return sessionKey.includes(":") ? sessionKey.split(":").pop() : sessionKey;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Creates an empty timestamped file and updates the "current" pointer.
|
|
51
|
+
* Used only by tests for placeholder-file scenarios.
|
|
52
|
+
*/
|
|
53
|
+
export function createNewSessionFile(sessionDir) {
|
|
54
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
55
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
56
|
+
const uuid = randomUUID().slice(0, 8);
|
|
57
|
+
const filename = `${timestamp}_${uuid}.jsonl`;
|
|
58
|
+
const filePath = join(sessionDir, filename);
|
|
59
|
+
writeFileSync(join(sessionDir, "current"), filename, "utf-8");
|
|
60
|
+
writeFileSync(filePath, "", "utf-8");
|
|
61
|
+
return filePath;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new persistent session file with a proper SessionManager header and cwd.
|
|
65
|
+
* Also updates the "current" pointer.
|
|
66
|
+
*/
|
|
67
|
+
export function createManagedSessionFile(sessionDir, cwd) {
|
|
68
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
69
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
70
|
+
const sessionId = randomUUID();
|
|
71
|
+
const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);
|
|
72
|
+
writeSessionHeader(sessionFile, cwd, sessionId);
|
|
73
|
+
setCurrentPointer(sessionDir, sessionFile);
|
|
74
|
+
return sessionFile;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Open a session file with an explicit cwd, even if the file does not exist yet.
|
|
78
|
+
* This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.
|
|
79
|
+
*/
|
|
80
|
+
export function openManagedSession(sessionFile, sessionDir, cwd) {
|
|
81
|
+
const SessionManagerCtor = SessionManager;
|
|
82
|
+
return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);
|
|
83
|
+
}
|
|
84
|
+
function setCurrentPointer(sessionDir, sessionFilePath) {
|
|
85
|
+
const filename = sessionFilePath.split("/").pop();
|
|
86
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
87
|
+
writeFileSync(join(sessionDir, "current"), filename, "utf-8");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates or overwrites a fixed-path session file with a valid session header.
|
|
91
|
+
*/
|
|
92
|
+
export function createManagedSessionFileAtPath(sessionFile, cwd) {
|
|
93
|
+
writeSessionHeader(sessionFile, cwd);
|
|
94
|
+
return sessionFile;
|
|
95
|
+
}
|
|
96
|
+
function writeSessionHeader(sessionFile, cwd, sessionId = randomUUID()) {
|
|
97
|
+
const sessionDir = getFileDir(sessionFile);
|
|
98
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
99
|
+
const header = {
|
|
100
|
+
type: "session",
|
|
101
|
+
version: 3,
|
|
102
|
+
id: sessionId,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
cwd,
|
|
105
|
+
};
|
|
106
|
+
writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, "utf-8");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns the channel-level session directory: {channelDir}/sessions/
|
|
110
|
+
*/
|
|
111
|
+
export function getChannelSessionDir(channelDir) {
|
|
112
|
+
return join(channelDir, "sessions");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the fixed session file path for a Slack thread.
|
|
116
|
+
*/
|
|
117
|
+
export function getThreadSessionFile(channelDir, sessionKey) {
|
|
118
|
+
return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);
|
|
119
|
+
}
|
|
120
|
+
function hasSessionHeader(sessionFile) {
|
|
121
|
+
try {
|
|
122
|
+
const lines = readFileSync(sessionFile, "utf-8").split("\n");
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed)
|
|
126
|
+
continue;
|
|
127
|
+
const entry = JSON.parse(trimmed);
|
|
128
|
+
return entry.type === "session";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function getFileDir(sessionFile) {
|
|
137
|
+
return sessionFile.substring(0, sessionFile.lastIndexOf("/"));
|
|
138
|
+
}
|
|
139
|
+
function getCurrentSessionPath(sessionDir) {
|
|
140
|
+
const pointerFile = join(sessionDir, "current");
|
|
141
|
+
if (!existsSync(pointerFile))
|
|
142
|
+
return null;
|
|
143
|
+
const filename = readFileSync(pointerFile, "utf-8").trim();
|
|
144
|
+
if (!filename)
|
|
145
|
+
return null;
|
|
146
|
+
return join(sessionDir, filename);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Try to resolve an existing current session file.
|
|
150
|
+
* Returns null if no current pointer exists or the pointed file has no valid session header.
|
|
151
|
+
*/
|
|
152
|
+
export function tryResolveCurrentSession(sessionDir) {
|
|
153
|
+
const fullPath = getCurrentSessionPath(sessionDir);
|
|
154
|
+
if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath))
|
|
155
|
+
return fullPath;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Try to resolve an existing thread session file.
|
|
160
|
+
* Returns the file path if found, or null if no valid thread session exists yet.
|
|
161
|
+
*/
|
|
162
|
+
export function tryResolveThreadSession(sessionFile) {
|
|
163
|
+
return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the channel's current session file path (for fork source).
|
|
167
|
+
* Returns null if no channel session exists.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveChannelSessionFile(channelDir) {
|
|
170
|
+
const channelSessionDir = getChannelSessionDir(channelDir);
|
|
171
|
+
return tryResolveCurrentSession(channelSessionDir);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Fork a channel session into a fixed thread-session path.
|
|
175
|
+
* The resulting file keeps forkFrom's distinct session/header metadata.
|
|
176
|
+
*/
|
|
177
|
+
export function forkThreadSessionFile(sourceSessionFile, targetSessionFile, cwd) {
|
|
178
|
+
const sessionDir = getFileDir(targetSessionFile);
|
|
179
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
180
|
+
const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);
|
|
181
|
+
const forkedFile = forked.getSessionFile();
|
|
182
|
+
if (!forkedFile) {
|
|
183
|
+
throw new Error(`Failed to fork session from ${sourceSessionFile}`);
|
|
184
|
+
}
|
|
185
|
+
rmSync(targetSessionFile, { force: true });
|
|
186
|
+
renameSync(forkedFile, targetSessionFile);
|
|
187
|
+
return targetSessionFile;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@mariozechner/pi-coding-agent\";\n\n/**\n * Returns the shared session directory for a channel.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getSessionDir(channelDir: string, _sessionKey: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n writeFileSync(filePath, \"\", \"utf-8\");\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n writeFileSync(sessionFile, `${JSON.stringify(header)}\\n`, \"utf-8\");\n}\n\n/**\n * Returns the channel-level session directory: {channelDir}/sessions/\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n const channelSessionDir = getChannelSessionDir(channelDir);\n return tryResolveCurrentSession(channelSessionDir);\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminixiang/mama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"description": "Slack bot that delegates messages to the pi coding agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -39,17 +39,18 @@
|
|
|
39
39
|
"prepare": "husky"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.
|
|
42
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.43",
|
|
43
43
|
"@google-cloud/logging": "^11.2.1",
|
|
44
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
45
|
-
"@mariozechner/pi-ai": "^0.
|
|
46
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
47
|
-
"@
|
|
44
|
+
"@mariozechner/pi-agent-core": "^0.63.1",
|
|
45
|
+
"@mariozechner/pi-ai": "^0.63.1",
|
|
46
|
+
"@mariozechner/pi-coding-agent": "^0.63.1",
|
|
47
|
+
"@sentry/node": "^10.47.0",
|
|
48
|
+
"@sinclair/typebox": "^0.34.49",
|
|
48
49
|
"@slack/socket-mode": "^2.0.6",
|
|
49
50
|
"@slack/web-api": "^7.15.0",
|
|
50
51
|
"chalk": "^5.6.2",
|
|
51
52
|
"croner": "^10.0.1",
|
|
52
|
-
"diff": "^8.0.
|
|
53
|
+
"diff": "^8.0.4",
|
|
53
54
|
"discord.js": "^14.25.1",
|
|
54
55
|
"grammy": "^1.41.1",
|
|
55
56
|
"pino": "^10.3.1"
|
|
@@ -57,14 +58,14 @@
|
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/diff": "^8.0.0",
|
|
59
60
|
"@types/node": "^25.5.0",
|
|
60
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
61
|
+
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
61
62
|
"husky": "^9.1.7",
|
|
62
63
|
"lint-staged": "^16.4.0",
|
|
63
|
-
"oxfmt": "^0.
|
|
64
|
-
"oxlint": "^1.
|
|
64
|
+
"oxfmt": "^0.42.0",
|
|
65
|
+
"oxlint": "^1.57.0",
|
|
65
66
|
"shx": "^0.4.0",
|
|
66
|
-
"typescript": "^
|
|
67
|
-
"vitest": "^4.1.
|
|
67
|
+
"typescript": "^6.0.2",
|
|
68
|
+
"vitest": "^4.1.2"
|
|
68
69
|
},
|
|
69
70
|
"lint-staged": {
|
|
70
71
|
"*.ts": [
|