@geminixiang/mikan 0.3.2 → 0.4.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/CHANGELOG.md +36 -0
- package/dist/adapter.d.ts +1 -138
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +1 -4
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +25 -33
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +28 -0
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/discord/types.d.ts +6 -0
- package/dist/adapters/discord/types.d.ts.map +1 -0
- package/dist/adapters/discord/types.js +2 -0
- package/dist/adapters/discord/types.js.map +1 -0
- package/dist/adapters/intake.d.ts +11 -0
- package/dist/adapters/intake.d.ts.map +1 -0
- package/dist/adapters/intake.js +42 -0
- package/dist/adapters/intake.js.map +1 -0
- package/dist/adapters/shared.d.ts +7 -31
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +18 -2
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +14 -33
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +148 -116
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts +3 -4
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +97 -14
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +5 -20
- package/dist/adapters/slack/session.d.ts.map +1 -1
- package/dist/adapters/slack/session.js.map +1 -1
- package/dist/adapters/slack/types.d.ts +84 -0
- package/dist/adapters/slack/types.d.ts.map +1 -0
- package/dist/adapters/slack/types.js +2 -0
- package/dist/adapters/slack/types.js.map +1 -0
- package/dist/adapters/streaming.d.ts +18 -0
- package/dist/adapters/streaming.d.ts.map +1 -0
- package/dist/adapters/streaming.js +44 -0
- package/dist/adapters/streaming.js.map +1 -0
- package/dist/adapters/telegram/bot.d.ts +1 -4
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +32 -39
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +33 -0
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/types.d.ts +6 -0
- package/dist/adapters/telegram/types.d.ts.map +1 -0
- package/dist/adapters/telegram/types.js +2 -0
- package/dist/adapters/telegram/types.js.map +1 -0
- package/dist/adapters/types.d.ts +58 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent.d.ts +4 -16
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +33 -20
- package/dist/agent.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +1 -1
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/auto-reply.d.ts.map +1 -1
- package/dist/commands/auto-reply.js +1 -8
- package/dist/commands/auto-reply.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/model.d.ts +5 -8
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +15 -20
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +5 -10
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/parse.d.ts.map +1 -1
- package/dist/commands/parse.js +1 -4
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +23 -0
- package/dist/commands/registry.js.map +1 -1
- package/dist/commands/sandbox.d.ts +2 -5
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +11 -16
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/session-view.d.ts.map +1 -1
- package/dist/commands/session-view.js +10 -15
- package/dist/commands/session-view.js.map +1 -1
- package/dist/commands/types.d.ts +11 -2
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/config.d.ts +6 -28
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -41
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +1 -15
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +4 -44
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +24 -43
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +3 -7
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +8 -8
- package/dist/execution-resolver.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +2 -6
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -37
- 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 +16 -16
- package/dist/main.js.map +1 -1
- package/dist/observability/instrument.d.ts.map +1 -0
- package/dist/{instrument.js → observability/instrument.js} +2 -2
- package/dist/observability/instrument.js.map +1 -0
- package/dist/{sentry.d.ts → observability/sentry.d.ts} +10 -30
- package/dist/observability/sentry.d.ts.map +1 -0
- package/dist/{sentry.js → observability/sentry.js} +70 -6
- package/dist/observability/sentry.js.map +1 -0
- package/dist/observability/types.d.ts +47 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +2 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/{ui-copy.d.ts → platform-messages.d.ts} +1 -1
- package/dist/platform-messages.d.ts.map +1 -0
- package/dist/{ui-copy.js → platform-messages.js} +1 -1
- package/dist/platform-messages.js.map +1 -0
- package/dist/portal-shell.d.ts +2 -28
- package/dist/portal-shell.d.ts.map +1 -1
- package/dist/portal-shell.js +2 -2
- package/dist/portal-shell.js.map +1 -1
- package/dist/provisioner.d.ts +2 -23
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +1 -1
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +4 -19
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -1
- package/dist/runtime/conversation-orchestrator.js +17 -8
- package/dist/runtime/conversation-orchestrator.js.map +1 -1
- package/dist/runtime/session-runtime.d.ts +2 -23
- package/dist/runtime/session-runtime.d.ts.map +1 -1
- package/dist/runtime/session-runtime.js +7 -9
- package/dist/runtime/session-runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +35 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -1
- package/dist/sandbox/cloudflare.js +1 -1
- package/dist/sandbox/cloudflare.js.map +1 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +1 -4
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sessions/chat-session-manager.d.ts +2 -46
- package/dist/sessions/chat-session-manager.d.ts.map +1 -1
- package/dist/sessions/chat-session-manager.js +12 -40
- package/dist/sessions/chat-session-manager.js.map +1 -1
- package/dist/sessions/metadata.d.ts +1 -13
- package/dist/sessions/metadata.d.ts.map +1 -1
- package/dist/sessions/metadata.js.map +1 -1
- package/dist/sessions/policy.d.ts +3 -10
- package/dist/sessions/policy.d.ts.map +1 -1
- package/dist/sessions/policy.js.map +1 -1
- package/dist/sessions/store.d.ts +1 -12
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +4 -7
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +76 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +2 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/store.d.ts +2 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +1 -1
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +30 -36
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +207 -26
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/sandbox.d.ts.map +1 -1
- package/dist/tools/sandbox.js +1 -1
- package/dist/tools/sandbox.js.map +1 -1
- package/dist/tools/truncate.d.ts +2 -26
- package/dist/tools/truncate.d.ts.map +1 -1
- package/dist/tools/truncate.js.map +1 -1
- package/dist/tools/types.d.ts +54 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/trigger.d.ts +2 -13
- package/dist/trigger.d.ts.map +1 -1
- package/dist/trigger.js.map +1 -1
- package/dist/types.d.ts +307 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/date.d.ts +10 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +23 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/file-guards.d.ts.map +1 -0
- package/dist/utils/file-guards.js.map +1 -0
- package/dist/utils/fs-atomic.d.ts.map +1 -0
- package/dist/utils/fs-atomic.js.map +1 -0
- package/dist/utils/html.d.ts.map +1 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/http-body.d.ts +10 -0
- package/dist/utils/http-body.d.ts.map +1 -0
- package/dist/utils/http-body.js +34 -0
- package/dist/utils/http-body.js.map +1 -0
- package/dist/vault/index.d.ts +34 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/{vault.js → vault/index.js} +4 -4
- package/dist/vault/index.js.map +1 -0
- package/dist/{vault-routing.d.ts → vault/routing.d.ts} +2 -2
- package/dist/vault/routing.d.ts.map +1 -0
- package/dist/{vault-routing.js → vault/routing.js} +2 -2
- package/dist/vault/routing.js.map +1 -0
- package/dist/{vault.d.ts → vault/types.d.ts} +3 -34
- package/dist/vault/types.d.ts.map +1 -0
- package/dist/vault/types.js +2 -0
- package/dist/vault/types.js.map +1 -0
- package/dist/web/admin/portal.d.ts +5 -0
- package/dist/web/admin/portal.d.ts.map +1 -0
- package/dist/{admin → web/admin}/portal.js +140 -52
- package/dist/web/admin/portal.js.map +1 -0
- package/dist/web/admin/store.d.ts +13 -0
- package/dist/web/admin/store.d.ts.map +1 -0
- package/dist/web/admin/store.js +23 -0
- package/dist/web/admin/store.js.map +1 -0
- package/dist/web/admin/types.d.ts +28 -0
- package/dist/web/admin/types.d.ts.map +1 -0
- package/dist/web/admin/types.js +2 -0
- package/dist/web/admin/types.js.map +1 -0
- package/dist/web/login/oauth.d.ts +6 -0
- package/dist/web/login/oauth.d.ts.map +1 -0
- package/dist/{login/index.js → web/login/oauth.js} +107 -106
- package/dist/web/login/oauth.js.map +1 -0
- package/dist/{login → web/login}/portal.d.ts +5 -5
- package/dist/web/login/portal.d.ts.map +1 -0
- package/dist/{login → web/login}/portal.js +16 -35
- package/dist/web/login/portal.js.map +1 -0
- package/dist/web/login/store.d.ts +12 -0
- package/dist/web/login/store.d.ts.map +1 -0
- package/dist/web/login/store.js +28 -0
- package/dist/web/login/store.js.map +1 -0
- package/dist/web/login/types.d.ts +50 -0
- package/dist/web/login/types.d.ts.map +1 -0
- package/dist/web/login/types.js +2 -0
- package/dist/web/login/types.js.map +1 -0
- package/dist/web/session-view/command.d.ts +4 -0
- package/dist/web/session-view/command.d.ts.map +1 -0
- package/dist/{session-view → web/session-view}/command.js +1 -1
- package/dist/web/session-view/command.js.map +1 -0
- package/dist/{session-view → web/session-view}/portal.d.ts +2 -5
- package/dist/web/session-view/portal.d.ts.map +1 -0
- package/dist/{session-view → web/session-view}/portal.js +5 -5
- package/dist/web/session-view/portal.js.map +1 -0
- package/dist/web/session-view/service.d.ts +6 -0
- package/dist/web/session-view/service.d.ts.map +1 -0
- package/dist/{session-view → web/session-view}/service.js +6 -36
- package/dist/web/session-view/service.js.map +1 -0
- package/dist/web/session-view/store.d.ts +8 -0
- package/dist/web/session-view/store.d.ts.map +1 -0
- package/dist/web/session-view/store.js +20 -0
- package/dist/web/session-view/store.js.map +1 -0
- package/dist/{session-view/service.d.ts → web/session-view/types.d.ts} +20 -4
- package/dist/web/session-view/types.d.ts.map +1 -0
- package/dist/web/session-view/types.js +2 -0
- package/dist/web/session-view/types.js.map +1 -0
- package/dist/web/token-store.d.ts +19 -0
- package/dist/web/token-store.d.ts.map +1 -0
- package/dist/web/token-store.js +45 -0
- package/dist/web/token-store.js.map +1 -0
- package/dist/web/types.d.ts +5 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/package.json +1 -1
- package/dist/adapters/discord/index.d.ts +0 -3
- package/dist/adapters/discord/index.d.ts.map +0 -1
- package/dist/adapters/discord/index.js +0 -3
- package/dist/adapters/discord/index.js.map +0 -1
- package/dist/adapters/slack/index.d.ts +0 -3
- package/dist/adapters/slack/index.d.ts.map +0 -1
- package/dist/adapters/slack/index.js +0 -3
- package/dist/adapters/slack/index.js.map +0 -1
- package/dist/adapters/slack/thread-manager.d.ts +0 -19
- package/dist/adapters/slack/thread-manager.d.ts.map +0 -1
- package/dist/adapters/slack/thread-manager.js +0 -11
- package/dist/adapters/slack/thread-manager.js.map +0 -1
- package/dist/adapters/telegram/index.d.ts +0 -3
- package/dist/adapters/telegram/index.d.ts.map +0 -1
- package/dist/adapters/telegram/index.js +0 -3
- package/dist/adapters/telegram/index.js.map +0 -1
- package/dist/admin/portal.d.ts +0 -27
- package/dist/admin/portal.d.ts.map +0 -1
- package/dist/admin/portal.js.map +0 -1
- package/dist/admin/store.d.ts +0 -22
- package/dist/admin/store.d.ts.map +0 -1
- package/dist/admin/store.js +0 -39
- package/dist/admin/store.js.map +0 -1
- package/dist/commands/index.d.ts +0 -5
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js +0 -20
- package/dist/commands/index.js.map +0 -1
- package/dist/env.d.ts.map +0 -1
- package/dist/env.js.map +0 -1
- package/dist/file-guards.d.ts.map +0 -1
- package/dist/file-guards.js.map +0 -1
- package/dist/fs-atomic.d.ts.map +0 -1
- package/dist/fs-atomic.js.map +0 -1
- package/dist/html.d.ts.map +0 -1
- package/dist/html.js.map +0 -1
- package/dist/instrument.d.ts.map +0 -1
- package/dist/instrument.js.map +0 -1
- package/dist/login/index.d.ts +0 -43
- package/dist/login/index.d.ts.map +0 -1
- package/dist/login/index.js.map +0 -1
- package/dist/login/portal.d.ts.map +0 -1
- package/dist/login/portal.js.map +0 -1
- package/dist/login/store.d.ts +0 -26
- package/dist/login/store.d.ts.map +0 -1
- package/dist/login/store.js +0 -56
- package/dist/login/store.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -2
- package/dist/runtime/index.js.map +0 -1
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js.map +0 -1
- package/dist/session-view/command.d.ts +0 -5
- package/dist/session-view/command.d.ts.map +0 -1
- package/dist/session-view/command.js.map +0 -1
- package/dist/session-view/portal.d.ts.map +0 -1
- package/dist/session-view/portal.js.map +0 -1
- package/dist/session-view/service.d.ts.map +0 -1
- package/dist/session-view/service.js.map +0 -1
- package/dist/session-view/store.d.ts +0 -18
- package/dist/session-view/store.d.ts.map +0 -1
- package/dist/session-view/store.js +0 -36
- package/dist/session-view/store.js.map +0 -1
- package/dist/ui-copy.d.ts.map +0 -1
- package/dist/ui-copy.js.map +0 -1
- package/dist/vault-routing.d.ts.map +0 -1
- package/dist/vault-routing.js.map +0 -1
- package/dist/vault.d.ts.map +0 -1
- package/dist/vault.js.map +0 -1
- /package/dist/{instrument.d.ts → observability/instrument.d.ts} +0 -0
- /package/dist/{env.d.ts → utils/env.d.ts} +0 -0
- /package/dist/{env.js → utils/env.js} +0 -0
- /package/dist/{file-guards.d.ts → utils/file-guards.d.ts} +0 -0
- /package/dist/{file-guards.js → utils/file-guards.js} +0 -0
- /package/dist/{fs-atomic.d.ts → utils/fs-atomic.d.ts} +0 -0
- /package/dist/{fs-atomic.js → utils/fs-atomic.js} +0 -0
- /package/dist/{html.d.ts → utils/html.d.ts} +0 -0
- /package/dist/{html.js → utils/html.js} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ConversationKind } from "../adapter.js";
|
|
2
|
+
export interface MikanSessionHeader {
|
|
3
|
+
type?: string;
|
|
4
|
+
version?: number;
|
|
5
|
+
id?: string;
|
|
6
|
+
timestamp?: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
parentSession?: string;
|
|
9
|
+
source?: {
|
|
10
|
+
kind?: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
recentDays?: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ResolveSessionKeyOptions {
|
|
16
|
+
conversationId: string;
|
|
17
|
+
conversationKind: ConversationKind;
|
|
18
|
+
messageId: string;
|
|
19
|
+
threadTs?: string;
|
|
20
|
+
persistentTopLevel?: boolean;
|
|
21
|
+
scopeDirectThreads?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ThreadRootMessage {
|
|
24
|
+
text?: string;
|
|
25
|
+
userName?: string;
|
|
26
|
+
user?: string;
|
|
27
|
+
loggedAt?: number;
|
|
28
|
+
isBot?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolvedSessionScope {
|
|
31
|
+
sessionDir: string;
|
|
32
|
+
contextFile: string;
|
|
33
|
+
threadRootMessage: ThreadRootMessage | null;
|
|
34
|
+
}
|
|
35
|
+
export interface ChatSessionManagerOptions {
|
|
36
|
+
recentDays?: number;
|
|
37
|
+
maxTopLevelMessages?: number;
|
|
38
|
+
now?: () => Date;
|
|
39
|
+
}
|
|
40
|
+
export interface ResolveChatSessionScopeOptions {
|
|
41
|
+
conversationDir: string;
|
|
42
|
+
sessionKey: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
/** The triggering platform message ID. Excluded from bootstrap to avoid duplicate user turns. */
|
|
45
|
+
currentMessageId?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface SyncChatSessionManagerOptions {
|
|
48
|
+
conversationDir: string;
|
|
49
|
+
sessionKey: string;
|
|
50
|
+
sessionManager: import("@earendil-works/pi-coding-agent").SessionManager;
|
|
51
|
+
/** The triggering platform message ID. Excluded from sync to avoid duplicate user turns. */
|
|
52
|
+
currentMessageId?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ResetChatSessionOptions {
|
|
55
|
+
conversationDir: string;
|
|
56
|
+
sessionKey: string;
|
|
57
|
+
cwd?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface RegisterThreadSessionOptions {
|
|
60
|
+
conversationDir: string;
|
|
61
|
+
sessionKey: string;
|
|
62
|
+
cwd?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface HasMaterializedSessionOptions {
|
|
65
|
+
conversationDir: string;
|
|
66
|
+
sessionKey: string;
|
|
67
|
+
}
|
|
68
|
+
export interface ThreadBootstrapWaitOptions {
|
|
69
|
+
parentSessionKey: string;
|
|
70
|
+
sessionKey: string;
|
|
71
|
+
hasThreadSession: () => boolean;
|
|
72
|
+
isParentRunning: () => boolean;
|
|
73
|
+
sleep?: (ms: number) => Promise<void>;
|
|
74
|
+
pollMs?: number;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sessions/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAID,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAID,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC7C;AAID,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,OAAO,iCAAiC,EAAE,cAAc,CAAC;IACzE,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB","sourcesContent":["import type { ConversationKind } from \"../adapter.js\";\n\n// ── session metadata ─────────────────────────────────────────────────────────\n\nexport interface MikanSessionHeader {\n type?: string;\n version?: number;\n id?: string;\n timestamp?: string;\n cwd?: string;\n parentSession?: string;\n source?: {\n kind?: string;\n file?: string;\n recentDays?: number;\n };\n}\n\n// ── session policy ───────────────────────────────────────────────────────────\n\nexport interface ResolveSessionKeyOptions {\n conversationId: string;\n conversationKind: ConversationKind;\n messageId: string;\n threadTs?: string;\n persistentTopLevel?: boolean;\n scopeDirectThreads?: boolean;\n}\n\n// ── session store ────────────────────────────────────────────────────────────\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n isBot?: boolean;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\n// ── chat session manager ─────────────────────────────────────────────────────\n\nexport interface ChatSessionManagerOptions {\n recentDays?: number;\n maxTopLevelMessages?: number;\n now?: () => Date;\n}\n\nexport interface ResolveChatSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n /** The triggering platform message ID. Excluded from bootstrap to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface SyncChatSessionManagerOptions {\n conversationDir: string;\n sessionKey: string;\n sessionManager: import(\"@earendil-works/pi-coding-agent\").SessionManager;\n /** The triggering platform message ID. Excluded from sync to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface ResetChatSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface RegisterThreadSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface HasMaterializedSessionOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\nexport interface ThreadBootstrapWaitOptions {\n parentSessionKey: string;\n sessionKey: string;\n hasThreadSession: () => boolean;\n isParentRunning: () => boolean;\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sessions/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ConversationKind } from \"../adapter.js\";\n\n// ── session metadata ─────────────────────────────────────────────────────────\n\nexport interface MikanSessionHeader {\n type?: string;\n version?: number;\n id?: string;\n timestamp?: string;\n cwd?: string;\n parentSession?: string;\n source?: {\n kind?: string;\n file?: string;\n recentDays?: number;\n };\n}\n\n// ── session policy ───────────────────────────────────────────────────────────\n\nexport interface ResolveSessionKeyOptions {\n conversationId: string;\n conversationKind: ConversationKind;\n messageId: string;\n threadTs?: string;\n persistentTopLevel?: boolean;\n scopeDirectThreads?: boolean;\n}\n\n// ── session store ────────────────────────────────────────────────────────────\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n isBot?: boolean;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\n// ── chat session manager ─────────────────────────────────────────────────────\n\nexport interface ChatSessionManagerOptions {\n recentDays?: number;\n maxTopLevelMessages?: number;\n now?: () => Date;\n}\n\nexport interface ResolveChatSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n /** The triggering platform message ID. Excluded from bootstrap to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface SyncChatSessionManagerOptions {\n conversationDir: string;\n sessionKey: string;\n sessionManager: import(\"@earendil-works/pi-coding-agent\").SessionManager;\n /** The triggering platform message ID. Excluded from sync to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface ResetChatSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface RegisterThreadSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface HasMaterializedSessionOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\nexport interface ThreadBootstrapWaitOptions {\n parentSessionKey: string;\n sessionKey: string;\n hasThreadSession: () => boolean;\n isParentRunning: () => boolean;\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n}\n"]}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
localPath: string;
|
|
4
|
-
}
|
|
5
|
-
export interface LoggedMessage {
|
|
6
|
-
date: string;
|
|
7
|
-
ts: string;
|
|
8
|
-
user: string;
|
|
9
|
-
userName?: string;
|
|
10
|
-
displayName?: string;
|
|
11
|
-
text: string;
|
|
12
|
-
attachments: Attachment[];
|
|
13
|
-
isBot: boolean;
|
|
14
|
-
threadTs?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface ChannelStoreConfig {
|
|
17
|
-
workingDir: string;
|
|
18
|
-
botToken: string;
|
|
19
|
-
}
|
|
1
|
+
export type { Attachment, ChannelStoreConfig, LoggedMessage } from "./types.js";
|
|
2
|
+
import type { Attachment, ChannelStoreConfig, LoggedMessage } from "./types.js";
|
|
20
3
|
export declare class ChannelStore {
|
|
21
4
|
private workingDir;
|
|
22
5
|
private botToken;
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAgBhF,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAMrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIvC;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMrE;IAED;;;OAGG;IACG,kBAAkB,CACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACpF,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CA8BvB;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA+B5E;IAED;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBjD;YAKa,2BAA2B;YAQ3B,kBAAkB;CAuBjC","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport {\n ensureDirExists,\n isRecord,\n parseJsonValue,\n readTextFileIfExists,\n} from \"./utils/file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\nimport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, writeFile } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { ensureDirExists, isRecord, parseJsonValue, readTextFileIfExists } from "./file-guards.js";
|
|
3
|
+
import { ensureDirExists, isRecord, parseJsonValue, readTextFileIfExists, } from "./utils/file-guards.js";
|
|
4
4
|
import { withRetry } from "./adapters/shared.js";
|
|
5
5
|
class AttachmentDownloadHttpError extends Error {
|
|
6
6
|
constructor(message, status) {
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { ensureDirExists, isRecord, parseJsonValue, readTextFileIfExists } from \"./file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport interface Attachment {\n original: string; // original filename from uploader\n localPath: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n date: string; // ISO 8601 date (e.g., \"2025-11-26T10:44:00.000Z\") for easy grepping\n ts: string; // slack timestamp or epoch ms\n user: string; // user ID (or \"bot\" for bot responses)\n userName?: string; // handle (e.g., \"mario\")\n displayName?: string; // display name (e.g., \"Mario Zechner\")\n text: string;\n attachments: Attachment[];\n isBot: boolean;\n threadTs?: string; // slack thread timestamp (root message ts)\n}\n\nexport interface ChannelStoreConfig {\n workingDir: string;\n botToken: string; // needed for authenticated file downloads\n}\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport {\n ensureDirExists,\n isRecord,\n parseJsonValue,\n readTextFileIfExists,\n} from \"./utils/file-guards.js\";\nimport { withRetry } from \"./adapters/shared.js\";\n\nexport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\nimport type { Attachment, ChannelStoreConfig, LoggedMessage } from \"./types.js\";\n\nclass AttachmentDownloadHttpError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nfunction isRetryableAttachmentDownloadError(error: unknown): boolean {\n if (!(error instanceof AttachmentDownloadHttpError)) return true;\n return error.status === 408 || error.status === 429 || error.status >= 500;\n}\n\nexport class ChannelStore {\n private workingDir: string;\n private botToken: string;\n // Track recently logged message timestamps to prevent duplicates\n // Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n private recentlyLogged = new Map<string, number>();\n\n constructor(config: ChannelStoreConfig) {\n this.workingDir = config.workingDir;\n this.botToken = config.botToken;\n\n // Ensure working directory exists\n ensureDirExists(this.workingDir);\n }\n\n /**\n * Get or create the directory for a channel/DM\n */\n getChannelDir(channelId: string): string {\n const channelDir = join(this.workingDir, channelId);\n ensureDirExists(channelDir);\n return channelDir;\n }\n\n /**\n * Generate a unique local filename for an attachment\n */\n generateLocalFilename(originalName: string, timestamp: string): string {\n // Convert slack timestamp (1234567890.123456) to milliseconds\n const ts = Math.floor(parseFloat(timestamp) * 1000);\n // Sanitize original name (remove problematic characters)\n const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n return `${ts}_${sanitized}`;\n }\n\n /**\n * Process attachments from a Slack message event.\n * Downloads files before returning so callers only receive readable paths.\n */\n async processAttachments(\n channelId: string,\n files: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n timestamp: string,\n ): Promise<Attachment[]> {\n const downloads: Array<Promise<Attachment>> = [];\n\n for (const file of files) {\n const url = file.url_private_download || file.url_private;\n if (!url) continue;\n if (!file.name) {\n throw new Error(`Attachment missing name for URL: ${url}`);\n }\n\n const filename = this.generateLocalFilename(file.name, timestamp);\n const localPath = `${channelId}/attachments/${filename}`;\n const attachment: Attachment = {\n original: file.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachmentWithRetry(localPath, url)\n .then(() => attachment)\n .catch((error) => {\n const errorMsg = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to download attachment ${localPath}: ${errorMsg}`, {\n cause: error,\n });\n }),\n );\n }\n\n return Promise.all(downloads);\n }\n\n /**\n * Log a message to the channel's log.jsonl\n * Returns false if message was already logged (duplicate)\n */\n async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n // Check for duplicate (same channel + timestamp)\n const dedupeKey = `${channelId}:${message.ts}`;\n if (this.recentlyLogged.has(dedupeKey)) {\n return false; // Already logged\n }\n\n const logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n // Ensure message has a date field\n if (!message.date) {\n // Parse timestamp to get date\n let date: Date;\n if (message.ts.includes(\".\")) {\n // Slack timestamp format (1234567890.123456)\n date = new Date(parseFloat(message.ts) * 1000);\n } else {\n // Epoch milliseconds\n date = new Date(parseInt(message.ts, 10));\n }\n message.date = date.toISOString();\n }\n\n const line = `${JSON.stringify(message)}\\n`;\n await appendFile(logPath, line, \"utf-8\");\n\n // Mark as logged only after the append succeeds. Otherwise a transient\n // write failure can make retries look like duplicates and drop messages.\n this.recentlyLogged.set(dedupeKey, Date.now());\n setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n return true;\n }\n\n /**\n * Log a bot response\n */\n async logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n await this.logMessage(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Get the timestamp of the last logged message for a channel\n * Returns null if no log exists\n */\n getLastTimestamp(channelId: string): string | null {\n const logPath = join(this.workingDir, channelId, \"log.jsonl\");\n const content = readTextFileIfExists(logPath);\n if (content === undefined) {\n return null;\n }\n\n try {\n const lines = content.trim().split(\"\\n\");\n if (lines.length === 0 || lines[0] === \"\") {\n return null;\n }\n const lastLine = lines[lines.length - 1];\n const message = parseJsonValue(\n lastLine,\n (value): value is LoggedMessage => isRecord(value) && typeof value.ts === \"string\",\n (detail) => (detail === \"unexpected JSON shape\" ? \"log entry missing timestamp\" : detail),\n );\n return message.ts;\n } catch {\n return null;\n }\n }\n\n /**\n * Download a single attachment\n */\n private async downloadAttachmentWithRetry(localPath: string, url: string): Promise<void> {\n await withRetry(() => this.downloadAttachment(localPath, url), {\n maxAttempts: 3,\n baseDelayMs: 250,\n isRateLimited: isRetryableAttachmentDownloadError,\n });\n }\n\n private async downloadAttachment(localPath: string, url: string): Promise<void> {\n const filePath = join(this.workingDir, localPath);\n\n // Ensure directory exists\n const parentDir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n ensureDirExists(parentDir);\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.botToken}`,\n },\n });\n\n if (!response.ok) {\n throw new AttachmentDownloadHttpError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n );\n }\n\n const buffer = await response.arrayBuffer();\n await writeFile(filePath, Buffer.from(buffer));\n }\n}\n"]}
|
package/dist/tools/event.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ConversationKind } from "../adapter.js";
|
|
2
3
|
declare const eventSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
action: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"list">, import("@sinclair/typebox").TLiteral<"read">, import("@sinclair/typebox").TLiteral<"update">, import("@sinclair/typebox").TLiteral<"delete">]>>;
|
|
5
|
+
filename: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
|
+
scope: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"conversation">, import("@sinclair/typebox").TLiteral<"all">]>>;
|
|
7
|
+
label: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
8
|
+
type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"immediate">, import("@sinclair/typebox").TLiteral<"one-shot">, import("@sinclair/typebox").TLiteral<"periodic">]>>;
|
|
9
|
+
text: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
10
|
at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
7
11
|
schedule: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
8
12
|
timezone: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -11,40 +15,11 @@ declare const eventSchema: import("@sinclair/typebox").TObject<{
|
|
|
11
15
|
interface EventToolContext {
|
|
12
16
|
platform: string;
|
|
13
17
|
conversationId: string;
|
|
14
|
-
conversationKind:
|
|
18
|
+
conversationKind: ConversationKind;
|
|
15
19
|
userId: string;
|
|
16
20
|
}
|
|
17
|
-
export type EventPayload
|
|
18
|
-
|
|
19
|
-
platform: string;
|
|
20
|
-
conversationId: string;
|
|
21
|
-
conversationKind: "direct" | "shared";
|
|
22
|
-
userId: string;
|
|
23
|
-
text: string;
|
|
24
|
-
} | {
|
|
25
|
-
type: "one-shot";
|
|
26
|
-
platform: string;
|
|
27
|
-
conversationId: string;
|
|
28
|
-
conversationKind: "direct" | "shared";
|
|
29
|
-
userId: string;
|
|
30
|
-
text: string;
|
|
31
|
-
at: string;
|
|
32
|
-
} | {
|
|
33
|
-
type: "periodic";
|
|
34
|
-
platform: string;
|
|
35
|
-
conversationId: string;
|
|
36
|
-
conversationKind: "direct" | "shared";
|
|
37
|
-
userId: string;
|
|
38
|
-
text: string;
|
|
39
|
-
schedule: string;
|
|
40
|
-
timezone: string;
|
|
41
|
-
};
|
|
42
|
-
export interface EventStore {
|
|
43
|
-
write(filename: string, payload: EventPayload): Promise<{
|
|
44
|
-
path: string;
|
|
45
|
-
size: number;
|
|
46
|
-
}>;
|
|
47
|
-
}
|
|
21
|
+
export type { EventPayload, EventStore } from "./types.js";
|
|
22
|
+
import type { EventPayload, EventStore } from "./types.js";
|
|
48
23
|
export declare class HostEventStore implements EventStore {
|
|
49
24
|
private readonly eventsDir;
|
|
50
25
|
constructor(eventsDir: string);
|
|
@@ -53,10 +28,29 @@ export declare class HostEventStore implements EventStore {
|
|
|
53
28
|
path: string;
|
|
54
29
|
size: number;
|
|
55
30
|
}>;
|
|
31
|
+
list(): Promise<Array<{
|
|
32
|
+
filename: string;
|
|
33
|
+
payload: EventPayload;
|
|
34
|
+
size: number;
|
|
35
|
+
mtimeMs: number;
|
|
36
|
+
}>>;
|
|
37
|
+
read(filename: string): Promise<{
|
|
38
|
+
filename: string;
|
|
39
|
+
payload: EventPayload;
|
|
40
|
+
size: number;
|
|
41
|
+
mtimeMs: number;
|
|
42
|
+
}>;
|
|
43
|
+
update(filename: string, payload: EventPayload): Promise<{
|
|
44
|
+
path: string;
|
|
45
|
+
size: number;
|
|
46
|
+
}>;
|
|
47
|
+
delete(filename: string): Promise<{
|
|
48
|
+
deleted: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
private writePayload;
|
|
56
51
|
}
|
|
57
52
|
export declare function createEventTool(eventStore: EventStore): {
|
|
58
53
|
tool: AgentTool<typeof eventSchema>;
|
|
59
54
|
setEventContext: (context: EventToolContext) => void;
|
|
60
55
|
};
|
|
61
|
-
export {};
|
|
62
56
|
//# sourceMappingURL=event.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,QAAA,MAAM,WAAW;;;;;;;;EA6Bf,CAAC;AAEH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAYD,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEN,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzF;AAED,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,MAAM,EAAI;IAElD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAE5D;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAM5F;CACF;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG;IACvD,IAAI,EAAE,SAAS,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtD,CA8DA","sourcesContent":["import { mkdir, stat, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport * as log from \"../log.js\";\n\nconst eventSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of the event you're scheduling (shown to user)\",\n }),\n type: Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n text: Type.String({\n description:\n \"A self-contained task for the future run. Include the necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history.\",\n }),\n at: Type.Optional(\n Type.String({\n description: \"ISO 8601 timestamp with offset, required for one-shot events\",\n }),\n ),\n schedule: Type.Optional(\n Type.String({\n description: \"Cron schedule, required for periodic events\",\n }),\n ),\n timezone: Type.Optional(\n Type.String({\n description: \"IANA timezone, required for periodic events\",\n }),\n ),\n filenamePrefix: Type.Optional(\n Type.String({\n description: \"Optional filename prefix for the event file\",\n }),\n ),\n});\n\ninterface EventToolContext {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n}\n\ntype EventToolParams = {\n label: string;\n type: \"immediate\" | \"one-shot\" | \"periodic\";\n text: string;\n at?: string;\n schedule?: string;\n timezone?: string;\n filenamePrefix?: string;\n};\n\nexport type EventPayload =\n | {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n }\n | {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n at: string;\n }\n | {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n schedule: string;\n timezone: string;\n };\n\nexport interface EventStore {\n write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }>;\n}\n\nexport class HostEventStore implements EventStore {\n constructor(private readonly eventsDir: string) {}\n\n static fromWorkspaceDir(workspaceDir: string): HostEventStore {\n return new HostEventStore(join(workspaceDir, \"events\"));\n }\n\n async write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n await mkdir(this.eventsDir, { recursive: true });\n const filePath = join(this.eventsDir, filename);\n await writeFile(filePath, JSON.stringify(payload) + \"\\n\", \"utf-8\");\n const fileStat = await stat(filePath);\n return { path: filePath, size: fileStat.size };\n }\n}\n\nexport function createEventTool(eventStore: EventStore): {\n tool: AgentTool<typeof eventSchema>;\n setEventContext: (context: EventToolContext) => void;\n} {\n let eventContext: EventToolContext | null = null;\n\n const tool: AgentTool<typeof eventSchema> = {\n name: \"event\",\n label: \"event\",\n description:\n \"Schedule an immediate, one-shot, or periodic event for the current conversation. Write text as a self-contained task with any needed context, tone, or constraints because events do not inherit normal conversation history. This automatically writes to the correct events directory and fills the current platform, conversation, conversation kind, and requester userId.\",\n parameters: eventSchema,\n execute: async (_toolCallId: string, params: EventToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n if (!eventContext) {\n throw new Error(\"Event context not configured\");\n }\n\n const payload = buildEventPayload(params, eventContext);\n const prefix = sanitizeFileSegment(params.filenamePrefix || payload.type || \"event\");\n const filename = `${prefix}-${Date.now()}.json`;\n\n log.logInfo(\n `Writing event file via control plane store: ${filename} (type=${payload.type}, platform=${payload.platform}, conversation=${payload.conversationId})`,\n );\n\n try {\n const result = await eventStore.write(filename, payload);\n log.logInfo(\n `Wrote event file via control plane store: ${result.path} (${result.size} bytes)`,\n );\n } catch (err) {\n log.logWarning(\n `Failed to write event file via control plane store: ${filename}`,\n String(err),\n );\n throw err;\n }\n\n return {\n content: [\n {\n type: \"text\",\n text:\n payload.type === \"periodic\"\n ? `Scheduled periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`\n : payload.type === \"one-shot\"\n ? `Scheduled one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`\n : `Queued immediate event ${filename} for ${payload.platform}/${payload.conversationId}`,\n },\n ],\n details: undefined,\n };\n },\n };\n\n return {\n tool,\n setEventContext: (context: EventToolContext) => {\n eventContext = context;\n },\n };\n}\n\nfunction buildEventPayload(params: EventToolParams, context: EventToolContext): EventPayload {\n const base = {\n platform: context.platform,\n conversationId: context.conversationId,\n conversationKind: context.conversationKind,\n userId: context.userId,\n text: params.text,\n };\n\n if (params.type === \"immediate\") {\n return {\n ...base,\n type: \"immediate\",\n };\n }\n\n if (params.type === \"one-shot\") {\n if (!params.at) {\n throw new Error(\"`at` is required for one-shot events\");\n }\n\n const atTime = new Date(params.at).getTime();\n if (Number.isNaN(atTime)) {\n throw new Error(\"`at` must be a valid ISO 8601 timestamp with UTC offset\");\n }\n if (atTime <= Date.now()) {\n throw new Error(\n `\\`at\\` must be in the future; got ${params.at} (now=${new Date().toISOString()}). Check the timezone offset.`,\n );\n }\n\n // No sessionKey or threadTs: reminders should fire as top-level messages, not buried in old threads\n return { ...base, type: \"one-shot\", at: params.at };\n }\n\n if (!params.schedule) {\n throw new Error(\"`schedule` is required for periodic events\");\n }\n if (!params.timezone) {\n throw new Error(\"`timezone` is required for periodic events\");\n }\n return {\n ...base,\n type: \"periodic\",\n schedule: params.schedule,\n timezone: params.timezone,\n };\n}\n\nfunction sanitizeFileSegment(value: string): string {\n const sanitized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"event\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtD,QAAA,MAAM,WAAW;;;;;;;;;;;EA0Df,CAAC;AAEH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAeD,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3D,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,MAAM,EAAI;IAElD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAE5D;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE5F;IAEK,IAAI,IAAI,OAAO,CACnB,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAClF,CASA;IAEK,IAAI,CACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAWrF;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7F;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAI5D;YAEa,YAAY;CAiB3B;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG;IACvD,IAAI,EAAE,SAAS,CAAC,OAAO,WAAW,CAAC,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtD,CAuGA","sourcesContent":["import { constants } from \"node:fs\";\nimport { mkdir, open, readdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { ConversationKind } from \"../adapter.js\";\nimport * as log from \"../log.js\";\n\nconst eventSchema = Type.Object({\n action: Type.Optional(\n Type.Union(\n [\n Type.Literal(\"create\"),\n Type.Literal(\"list\"),\n Type.Literal(\"read\"),\n Type.Literal(\"update\"),\n Type.Literal(\"delete\"),\n ],\n { description: \"CRUD action. Defaults to create for backward compatibility.\" },\n ),\n ),\n filename: Type.Optional(\n Type.String({\n description: \"Event filename for read, update, or delete actions\",\n }),\n ),\n scope: Type.Optional(\n Type.Union([Type.Literal(\"conversation\"), Type.Literal(\"all\")], {\n description:\n \"List scope. Defaults to conversation, which only shows events for the current conversation. Use all only when the user explicitly asks for all events.\",\n }),\n ),\n label: Type.Optional(\n Type.String({\n description: \"Brief description of the event you're scheduling (shown to user)\",\n }),\n ),\n type: Type.Optional(\n Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n ),\n text: Type.Optional(\n Type.String({\n description:\n \"A self-contained task for the future run. Include the necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history.\",\n }),\n ),\n at: Type.Optional(\n Type.String({\n description: \"ISO 8601 timestamp with offset, required for one-shot events\",\n }),\n ),\n schedule: Type.Optional(\n Type.String({\n description: \"Cron schedule, required for periodic events\",\n }),\n ),\n timezone: Type.Optional(\n Type.String({\n description: \"IANA timezone, required for periodic events\",\n }),\n ),\n filenamePrefix: Type.Optional(\n Type.String({\n description: \"Optional filename prefix for the event file\",\n }),\n ),\n});\n\ninterface EventToolContext {\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId: string;\n}\n\ntype EventToolParams = {\n action?: \"create\" | \"list\" | \"read\" | \"update\" | \"delete\";\n filename?: string;\n scope?: \"conversation\" | \"all\";\n label?: string;\n type?: \"immediate\" | \"one-shot\" | \"periodic\";\n text?: string;\n at?: string;\n schedule?: string;\n timezone?: string;\n filenamePrefix?: string;\n};\n\nexport type { EventPayload, EventStore } from \"./types.js\";\nimport type { EventPayload, EventStore } from \"./types.js\";\n\nexport class HostEventStore implements EventStore {\n constructor(private readonly eventsDir: string) {}\n\n static fromWorkspaceDir(workspaceDir: string): HostEventStore {\n return new HostEventStore(join(workspaceDir, \"events\"));\n }\n\n async write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n return this.writePayload(filename, payload);\n }\n\n async list(): Promise<\n Array<{ filename: string; payload: EventPayload; size: number; mtimeMs: number }>\n > {\n await mkdir(this.eventsDir, { recursive: true });\n const entries = await readdir(this.eventsDir, { withFileTypes: true });\n const events = await Promise.all(\n entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map(async (entry) => this.read(entry.name)),\n );\n return events.toSorted((a, b) => a.filename.localeCompare(b.filename));\n }\n\n async read(\n filename: string,\n ): Promise<{ filename: string; payload: EventPayload; size: number; mtimeMs: number }> {\n const safeFilename = validateEventFilename(filename);\n const filePath = join(this.eventsDir, safeFilename);\n const [raw, fileStat] = await Promise.all([readFile(filePath, \"utf-8\"), stat(filePath)]);\n const parsed = JSON.parse(raw) as EventPayload;\n return {\n filename: safeFilename,\n payload: parsed,\n size: fileStat.size,\n mtimeMs: fileStat.mtimeMs,\n };\n }\n\n async update(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n return this.writePayload(filename, payload, constants.O_WRONLY | constants.O_TRUNC);\n }\n\n async delete(filename: string): Promise<{ deleted: boolean }> {\n const safeFilename = validateEventFilename(filename);\n await rm(join(this.eventsDir, safeFilename), { force: true });\n return { deleted: true };\n }\n\n private async writePayload(\n filename: string,\n payload: EventPayload,\n flag?: string | number,\n ): Promise<{ path: string; size: number }> {\n await mkdir(this.eventsDir, { recursive: true });\n const safeFilename = validateEventFilename(filename);\n const filePath = join(this.eventsDir, safeFilename);\n if (flag === undefined) {\n await writeFile(filePath, JSON.stringify(payload) + \"\\n\", \"utf-8\");\n } else {\n await using file = await open(filePath, flag);\n await file.writeFile(JSON.stringify(payload) + \"\\n\", \"utf-8\");\n }\n const fileStat = await stat(filePath);\n return { path: filePath, size: fileStat.size };\n }\n}\n\nexport function createEventTool(eventStore: EventStore): {\n tool: AgentTool<typeof eventSchema>;\n setEventContext: (context: EventToolContext) => void;\n} {\n let eventContext: EventToolContext | null = null;\n\n const tool: AgentTool<typeof eventSchema> = {\n name: \"event\",\n label: \"event\",\n description:\n \"CRUD tool for scheduled events. Create immediate, one-shot, or periodic events for the current conversation. List defaults to events for the current conversation only; use scope=all only when the user explicitly asks to list all events. Event text must be self-contained because events do not inherit normal conversation history.\",\n parameters: eventSchema,\n execute: async (_toolCallId: string, params: EventToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n if (!eventContext) {\n throw new Error(\"Event context not configured\");\n }\n\n const action = params.action ?? \"create\";\n\n if (action === \"list\") {\n const events = await eventStore.list();\n const conversationId = eventContext.conversationId;\n const scopedEvents =\n params.scope === \"all\"\n ? events\n : events.filter((event) => event.payload.conversationId === conversationId);\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n scope: params.scope ?? \"conversation\",\n conversationId: params.scope === \"all\" ? undefined : conversationId,\n events: scopedEvents,\n },\n null,\n 2,\n ),\n },\n ],\n details: undefined,\n };\n }\n\n if (action === \"read\") {\n const filename = requireFilename(params);\n const event = await eventStore.read(filename);\n return {\n content: [{ type: \"text\", text: JSON.stringify(event, null, 2) }],\n details: undefined,\n };\n }\n\n if (action === \"delete\") {\n const filename = requireFilename(params);\n await eventStore.delete(filename);\n return {\n content: [{ type: \"text\", text: `Deleted event ${filename}` }],\n details: undefined,\n };\n }\n\n const payload = buildEventPayload(params, eventContext);\n const filename =\n action === \"update\"\n ? requireFilename(params)\n : `${sanitizeFileSegment(params.filenamePrefix || payload.type || \"event\")}-${Date.now()}.json`;\n\n log.logInfo(\n `${action === \"update\" ? \"Updating\" : \"Writing\"} event file via control plane store: ${filename} (type=${payload.type}, platform=${payload.platform}, conversation=${payload.conversationId})`,\n );\n\n try {\n const result =\n action === \"update\"\n ? await eventStore.update(filename, payload)\n : await eventStore.write(filename, payload);\n log.logInfo(\n `${action === \"update\" ? \"Updated\" : \"Wrote\"} event file via control plane store: ${result.path} (${result.size} bytes)`,\n );\n } catch (err) {\n log.logWarning(\n `Failed to ${action === \"update\" ? \"update\" : \"write\"} event file via control plane store: ${filename}`,\n String(err),\n );\n throw err;\n }\n\n return {\n content: [{ type: \"text\", text: formatEventWriteResult(action, filename, payload) }],\n details: undefined,\n };\n },\n };\n\n return {\n tool,\n setEventContext: (context: EventToolContext) => {\n eventContext = context;\n },\n };\n}\n\nfunction buildEventPayload(params: EventToolParams, context: EventToolContext): EventPayload {\n if (!params.type) {\n throw new Error(\"`type` is required for create and update actions\");\n }\n if (!params.text) {\n throw new Error(\"`text` is required for create and update actions\");\n }\n\n const base = {\n platform: context.platform,\n conversationId: context.conversationId,\n conversationKind: context.conversationKind,\n userId: context.userId,\n text: params.text,\n };\n\n if (params.type === \"immediate\") {\n return {\n ...base,\n type: \"immediate\",\n };\n }\n\n if (params.type === \"one-shot\") {\n if (!params.at) {\n throw new Error(\"`at` is required for one-shot events\");\n }\n\n const atTime = new Date(params.at).getTime();\n if (Number.isNaN(atTime)) {\n throw new Error(\"`at` must be a valid ISO 8601 timestamp with UTC offset\");\n }\n if (atTime <= Date.now()) {\n throw new Error(\n `\\`at\\` must be in the future; got ${params.at} (now=${new Date().toISOString()}). Check the timezone offset.`,\n );\n }\n\n // No sessionKey or threadTs: reminders should fire as top-level messages, not buried in old threads\n return { ...base, type: \"one-shot\", at: params.at };\n }\n\n if (!params.schedule) {\n throw new Error(\"`schedule` is required for periodic events\");\n }\n if (!params.timezone) {\n throw new Error(\"`timezone` is required for periodic events\");\n }\n return {\n ...base,\n type: \"periodic\",\n schedule: params.schedule,\n timezone: params.timezone,\n };\n}\n\nfunction formatEventWriteResult(\n action: \"create\" | \"update\",\n filename: string,\n payload: EventPayload,\n): string {\n const scheduledVerb = action === \"update\" ? \"Updated\" : \"Scheduled\";\n const immediateVerb = action === \"update\" ? \"Updated\" : \"Queued\";\n switch (payload.type) {\n case \"periodic\":\n return `${scheduledVerb} periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`;\n case \"one-shot\":\n return `${scheduledVerb} one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`;\n case \"immediate\":\n return `${immediateVerb} immediate event ${filename} for ${payload.platform}/${payload.conversationId}`;\n }\n}\n\nfunction requireFilename(params: EventToolParams): string {\n if (!params.filename) {\n throw new Error(\"`filename` is required for read, update, and delete actions\");\n }\n return validateEventFilename(params.filename);\n}\n\nfunction validateEventFilename(filename: string): string {\n const trimmed = filename.trim();\n if (\n !trimmed ||\n trimmed !== basename(trimmed) ||\n trimmed.includes(\"..\") ||\n !trimmed.endsWith(\".json\")\n ) {\n throw new Error(\"Invalid event filename\");\n }\n return trimmed;\n}\n\nfunction sanitizeFileSegment(value: string): string {\n const sanitized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"event\";\n}\n"]}
|