@xopcai/xopc 0.0.99 → 0.0.101
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/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/src/account-manager.d.ts +3 -3
- package/dist/extensions/telegram/src/account-manager.js +8 -8
- package/dist/extensions/telegram/src/account-manager.js.map +1 -1
- package/dist/extensions/telegram/src/actions/message-actions.d.ts +4 -0
- package/dist/extensions/telegram/src/actions/message-actions.js +71 -0
- package/dist/extensions/telegram/src/actions/message-actions.js.map +1 -0
- package/dist/extensions/telegram/src/adapters/config-adapter.js +9 -1
- package/dist/extensions/telegram/src/adapters/config-adapter.js.map +1 -1
- package/dist/extensions/telegram/src/adapters/config-surface.js +1 -1
- package/dist/extensions/telegram/src/adapters/config-surface.js.map +1 -1
- package/dist/extensions/telegram/src/adapters/outbound-adapter.d.ts +2 -1
- package/dist/extensions/telegram/src/adapters/outbound-adapter.js +18 -4
- package/dist/extensions/telegram/src/adapters/outbound-adapter.js.map +1 -1
- package/dist/extensions/telegram/src/adapters/security-adapter.js +2 -1
- package/dist/extensions/telegram/src/adapters/security-adapter.js.map +1 -1
- package/dist/extensions/telegram/src/adapters/streaming-adapter.js +90 -6
- package/dist/extensions/telegram/src/adapters/streaming-adapter.js.map +1 -1
- package/dist/extensions/telegram/src/adapters/types.d.ts +9 -1
- package/dist/extensions/telegram/src/api-root.d.ts +4 -0
- package/dist/extensions/telegram/src/api-root.js +42 -0
- package/dist/extensions/telegram/src/api-root.js.map +1 -0
- package/dist/extensions/telegram/src/approval-delivery.d.ts +9 -0
- package/dist/extensions/telegram/src/approval-delivery.js +36 -0
- package/dist/extensions/telegram/src/approval-delivery.js.map +1 -0
- package/dist/extensions/telegram/src/approval-store.d.ts +25 -0
- package/dist/extensions/telegram/src/approval-store.js +61 -0
- package/dist/extensions/telegram/src/approval-store.js.map +1 -0
- package/dist/extensions/telegram/src/config-schema.d.ts +185 -35
- package/dist/extensions/telegram/src/config-schema.js +67 -45
- package/dist/extensions/telegram/src/config-schema.js.map +1 -1
- package/dist/extensions/telegram/src/conversation-id.d.ts +6 -0
- package/dist/extensions/telegram/src/conversation-id.js +18 -0
- package/dist/extensions/telegram/src/conversation-id.js.map +1 -0
- package/dist/extensions/telegram/src/doctor.d.ts +5 -0
- package/dist/extensions/telegram/src/doctor.js +80 -0
- package/dist/extensions/telegram/src/doctor.js.map +1 -0
- package/dist/extensions/telegram/src/exec-approval-handler.d.ts +7 -0
- package/dist/extensions/telegram/src/exec-approval-handler.js +36 -0
- package/dist/extensions/telegram/src/exec-approval-handler.js.map +1 -0
- package/dist/extensions/telegram/src/exec-approvals.d.ts +8 -0
- package/dist/extensions/telegram/src/exec-approvals.js +20 -0
- package/dist/extensions/telegram/src/exec-approvals.js.map +1 -0
- package/dist/extensions/telegram/src/focus-handler.d.ts +12 -0
- package/dist/extensions/telegram/src/focus-handler.js +55 -0
- package/dist/extensions/telegram/src/focus-handler.js.map +1 -0
- package/dist/extensions/telegram/src/group-config-resolver.d.ts +12 -0
- package/dist/extensions/telegram/src/group-config-resolver.js +17 -0
- package/dist/extensions/telegram/src/group-config-resolver.js.map +1 -0
- package/dist/extensions/telegram/src/inbound-coalescer.d.ts +15 -0
- package/dist/extensions/telegram/src/inbound-coalescer.js +107 -0
- package/dist/extensions/telegram/src/inbound-coalescer.js.map +1 -0
- package/dist/extensions/telegram/src/inbound-processor.d.ts +1 -0
- package/dist/extensions/telegram/src/inbound-processor.js +41 -4
- package/dist/extensions/telegram/src/inbound-processor.js.map +1 -1
- package/dist/extensions/telegram/src/network-errors.d.ts +2 -0
- package/dist/extensions/telegram/src/network-errors.js +23 -0
- package/dist/extensions/telegram/src/network-errors.js.map +1 -0
- package/dist/extensions/telegram/src/plugin.d.ts +11 -0
- package/dist/extensions/telegram/src/plugin.js +191 -57
- package/dist/extensions/telegram/src/plugin.js.map +1 -1
- package/dist/extensions/telegram/src/polling-session.d.ts +17 -0
- package/dist/extensions/telegram/src/polling-session.js +112 -0
- package/dist/extensions/telegram/src/polling-session.js.map +1 -0
- package/dist/extensions/telegram/src/proxy-fetch.d.ts +2 -0
- package/dist/extensions/telegram/src/proxy-fetch.js +14 -0
- package/dist/extensions/telegram/src/proxy-fetch.js.map +1 -0
- package/dist/extensions/telegram/src/reactions.d.ts +14 -0
- package/dist/extensions/telegram/src/reactions.js +48 -0
- package/dist/extensions/telegram/src/reactions.js.map +1 -0
- package/dist/extensions/telegram/src/reply-params.d.ts +15 -0
- package/dist/extensions/telegram/src/reply-params.js +28 -0
- package/dist/extensions/telegram/src/reply-params.js.map +1 -0
- package/dist/extensions/telegram/src/startup-errors.d.ts +3 -0
- package/dist/extensions/telegram/src/startup-errors.js +27 -0
- package/dist/extensions/telegram/src/startup-errors.js.map +1 -0
- package/dist/extensions/telegram/src/telegram-actions.d.ts +25 -0
- package/dist/extensions/telegram/src/telegram-actions.js +65 -0
- package/dist/extensions/telegram/src/telegram-actions.js.map +1 -0
- package/dist/extensions/telegram/src/thread-bindings.d.ts +15 -0
- package/dist/extensions/telegram/src/thread-bindings.js +22 -0
- package/dist/extensions/telegram/src/thread-bindings.js.map +1 -0
- package/dist/extensions/telegram/src/token-resolver.d.ts +8 -0
- package/dist/extensions/telegram/src/token-resolver.js +34 -0
- package/dist/extensions/telegram/src/token-resolver.js.map +1 -0
- package/dist/extensions/telegram/src/update-offset-store.d.ts +9 -0
- package/dist/extensions/telegram/src/update-offset-store.js +66 -0
- package/dist/extensions/telegram/src/update-offset-store.js.map +1 -0
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/api/api.js +5 -3
- package/dist/extensions/weixin/src/api/api.js.map +1 -1
- package/dist/gateway/static/root/assets/{agents-4KTK-Ku7.js → agents-u8cmUfGQ.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-DkgInY2w.js → apps-page-DF8m9cUE.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-preview-renderer-DBAxQXb-.js → attachment-preview-renderer-CybMSIoS.js} +2 -2
- package/dist/gateway/static/root/assets/{attachment-process-heavy-Csq3TrrP.js → attachment-process-heavy-RnGYy9bC.js} +2 -2
- package/dist/gateway/static/root/assets/channels-settings-BM3doRvi.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-ah4zWEIP.js +8 -0
- package/dist/gateway/static/root/assets/{cron-api-BRDli2Mm.js → cron-api-DgMuL_2b.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-BzEr5Za8.js → cron-page-COfUPKfq.js} +1 -1
- package/dist/gateway/static/root/assets/{dist-BsHh1vX0.js → dist-Dzy3Q0ZH.js} +1 -1
- package/dist/gateway/static/root/assets/{docx-preview-Dxj1EkCO.js → docx-preview-CajIk2FR.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-TWt5q_ef.js → extension-debug-page-kyxxefKh.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-C3kV8ZvZ.js → extension-page-BVbCBHoI.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-Csy9LNC6.js → extension-settings-page-t1lb2A7v.js} +1 -1
- package/dist/gateway/static/root/assets/{fetch-BLLOP2CM.js → fetch-drJUxxKP.js} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-MXbM8HvY.js → field-primitives-6Vo1Yga4.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-CkoyqfRx.js → heartbeat-config-api-BF3psu4O.js} +1 -1
- package/dist/gateway/static/root/assets/index-DjNDHzwG.css +1 -0
- package/dist/gateway/static/root/assets/index-WAyKAilR.js +4840 -0
- package/dist/gateway/static/root/assets/{logs-page-B2vY41ph.js → logs-page-BTVPXcHV.js} +1 -1
- package/dist/gateway/static/root/assets/{note-detail-page-B7qE5pkO.js → note-detail-page-DqSAg_5Z.js} +1 -1
- package/dist/gateway/static/root/assets/{note-time-NBBReVR4.js → note-time-FSOjchic.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-CsWYaqYM.js → notes-page-DkRX7BE2.js} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-BPGY3SCT.js → sessions-page-Khdw35Zk.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-advanced-gate-DtA451RM.js → settings-advanced-gate-DspC8oEm.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-form-section-DYR_SPlX.js → settings-form-section-DuxvUuur.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-BJUgZQzN.js → settings-page-DN8JBZ8F.js} +2 -2
- package/dist/gateway/static/root/assets/{share-preview-page-3u8VFmdo.js → share-preview-page-UNAcbiMb.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-DuyhcH4U.js → skills-page-ByYT0fXZ.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-e2q2yjs4.js → theme-store-B6KXPHgK.js} +1 -1
- package/dist/gateway/static/root/assets/url-q28QhI7l.js +3 -0
- package/dist/gateway/static/root/assets/{utils-B4Vw3fnc.js → utils-DK_1H47k.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-BFY67Hhp.js → voice-api-key-field-BYN2EIMs.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-page.utils-yUJ5jexd.js → workflow-page.utils-Bi19YNTr.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-DIMHCpNt.js → workflows-page-DP57FEKW.js} +1 -1
- package/dist/gateway/static/root/index.html +5 -5
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.d.ts +4 -0
- package/dist/src/agent/agent-manager.js +84 -4
- package/dist/src/agent/agent-manager.js.map +1 -1
- package/dist/src/agent/child-agent-factory.d.ts +1 -0
- package/dist/src/agent/child-agent-factory.js +13 -10
- package/dist/src/agent/child-agent-factory.js.map +1 -1
- package/dist/src/agent/embedded/run-for-session.d.ts +1 -0
- package/dist/src/agent/embedded/run-for-session.js +2 -0
- package/dist/src/agent/embedded/run-for-session.js.map +1 -1
- package/dist/src/agent/embedded/run-turn.js +1 -17
- package/dist/src/agent/embedded/run-turn.js.map +1 -1
- package/dist/src/agent/embedded/session-runner.js +3 -1
- package/dist/src/agent/embedded/session-runner.js.map +1 -1
- package/dist/src/agent/embedded/session-tool-result-guard.d.ts +1 -0
- package/dist/src/agent/embedded/session-tool-result-guard.js +1 -0
- package/dist/src/agent/embedded/session-tool-result-guard.js.map +1 -1
- package/dist/src/agent/inbound/attachment-pipeline.d.ts +37 -0
- package/dist/src/agent/inbound/attachment-pipeline.js +169 -0
- package/dist/src/agent/inbound/attachment-pipeline.js.map +1 -0
- package/dist/src/agent/inbound/turn-dispatcher.d.ts +3 -5
- package/dist/src/agent/inbound/turn-dispatcher.js +1 -2
- package/dist/src/agent/inbound/turn-dispatcher.js.map +1 -1
- package/dist/src/agent/memory/index.d.ts +2 -0
- package/dist/src/agent/memory/index.js +2 -1
- package/dist/src/agent/memory/memory-flush.d.ts +27 -0
- package/dist/src/agent/memory/memory-flush.js +124 -0
- package/dist/src/agent/memory/memory-flush.js.map +1 -0
- package/dist/src/agent/messaging/outbound-coordinator.js +2 -1
- package/dist/src/agent/messaging/outbound-coordinator.js.map +1 -1
- package/dist/src/agent/messaging/stream-manager.d.ts +2 -0
- package/dist/src/agent/messaging/stream-manager.js +3 -0
- package/dist/src/agent/messaging/stream-manager.js.map +1 -1
- package/dist/src/agent/orchestration/agent-orchestrator.d.ts +0 -4
- package/dist/src/agent/orchestration/agent-orchestrator.js +40 -107
- package/dist/src/agent/orchestration/agent-orchestrator.js.map +1 -1
- package/dist/src/agent/prompt/contribution.d.ts +9 -0
- package/dist/src/agent/prompt/contribution.js +1 -0
- package/dist/src/agent/prompt/safety.js +0 -10
- package/dist/src/agent/prompt/safety.js.map +1 -1
- package/dist/src/agent/prompt/sanitize-for-prompt.d.ts +6 -0
- package/dist/src/agent/prompt/sanitize-for-prompt.js +13 -0
- package/dist/src/agent/prompt/sanitize-for-prompt.js.map +1 -0
- package/dist/src/agent/prompt/sections/behavior.d.ts +5 -0
- package/dist/src/agent/prompt/sections/behavior.js +50 -0
- package/dist/src/agent/prompt/sections/behavior.js.map +1 -0
- package/dist/src/agent/prompt/sections/memory-skills.d.ts +9 -0
- package/dist/src/agent/prompt/sections/memory-skills.js +59 -0
- package/dist/src/agent/prompt/sections/memory-skills.js.map +1 -0
- package/dist/src/agent/prompt/sections/messaging-runtime.d.ts +16 -0
- package/dist/src/agent/prompt/sections/messaging-runtime.js +69 -0
- package/dist/src/agent/prompt/sections/messaging-runtime.js.map +1 -0
- package/dist/src/agent/prompt/sections/project-context.d.ts +16 -0
- package/dist/src/agent/prompt/sections/project-context.js +58 -0
- package/dist/src/agent/prompt/sections/project-context.js.map +1 -0
- package/dist/src/agent/prompt/sections/tooling.d.ts +5 -0
- package/dist/src/agent/prompt/sections/tooling.js +119 -0
- package/dist/src/agent/prompt/sections/tooling.js.map +1 -0
- package/dist/src/agent/prompt/sections/workspace-runtime.d.ts +11 -0
- package/dist/src/agent/prompt/sections/workspace-runtime.js +32 -0
- package/dist/src/agent/prompt/sections/workspace-runtime.js.map +1 -0
- package/dist/src/agent/prompt/service-prompt-builder.d.ts +13 -0
- package/dist/src/agent/prompt/service-prompt-builder.js +54 -13
- package/dist/src/agent/prompt/service-prompt-builder.js.map +1 -1
- package/dist/src/agent/prompt/subagent-context.d.ts +15 -0
- package/dist/src/agent/prompt/subagent-context.js +68 -0
- package/dist/src/agent/prompt/subagent-context.js.map +1 -0
- package/dist/src/agent/prompt/system-prompt-params.d.ts +53 -0
- package/dist/src/agent/prompt/system-prompt-params.js +74 -0
- package/dist/src/agent/prompt/system-prompt-params.js.map +1 -0
- package/dist/src/agent/prompt/system-prompt.d.ts +22 -11
- package/dist/src/agent/prompt/system-prompt.js +79 -193
- package/dist/src/agent/prompt/system-prompt.js.map +1 -1
- package/dist/src/agent/prompt/types.d.ts +3 -0
- package/dist/src/agent/service/build-direct-message-content.d.ts +5 -21
- package/dist/src/agent/service/build-direct-message-content.js +11 -64
- package/dist/src/agent/service/build-direct-message-content.js.map +1 -1
- package/dist/src/agent/service/direct-turn-helpers.d.ts +1 -1
- package/dist/src/agent/service/direct-turn-helpers.js +7 -0
- package/dist/src/agent/service/direct-turn-helpers.js.map +1 -1
- package/dist/src/agent/service/process-direct-one-shot.d.ts +3 -3
- package/dist/src/agent/service/process-direct-one-shot.js +22 -17
- package/dist/src/agent/service/process-direct-one-shot.js.map +1 -1
- package/dist/src/agent/service/process-direct-streaming.d.ts +6 -20
- package/dist/src/agent/service/process-direct-streaming.js +39 -40
- package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
- package/dist/src/agent/service/webchat-tts.d.ts +4 -9
- package/dist/src/agent/service/webchat-tts.js +9 -20
- package/dist/src/agent/service/webchat-tts.js.map +1 -1
- package/dist/src/agent/service.d.ts +8 -19
- package/dist/src/agent/service.js +53 -11
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/agent/tools/factory.js +2 -0
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/agent/tools/index.d.ts +1 -0
- package/dist/src/agent/tools/index.js +2 -1
- package/dist/src/agent/tools/media-read-tool.d.ts +2 -0
- package/dist/src/agent/tools/media-read-tool.js +89 -0
- package/dist/src/agent/tools/media-read-tool.js.map +1 -0
- package/dist/src/auth/credentials.js +0 -1
- package/dist/src/auth/credentials.js.map +1 -1
- package/dist/src/channels/attachments/attachment-classifiers.d.ts +8 -0
- package/dist/src/channels/attachments/attachment-classifiers.js +11 -0
- package/dist/src/channels/attachments/attachment-classifiers.js.map +1 -0
- package/dist/src/channels/attachments/inbound-persist.d.ts +13 -34
- package/dist/src/channels/attachments/inbound-persist.js +34 -102
- package/dist/src/channels/attachments/inbound-persist.js.map +1 -1
- package/dist/src/channels/attachments/inbound-types.d.ts +13 -0
- package/dist/src/channels/attachments/inbound-types.js +30 -0
- package/dist/src/channels/attachments/inbound-types.js.map +1 -0
- package/dist/src/channels/attachments/outbound-tts-persist.d.ts +3 -12
- package/dist/src/channels/attachments/outbound-tts-persist.js +28 -48
- package/dist/src/channels/attachments/outbound-tts-persist.js.map +1 -1
- package/dist/src/channels/attachments/voice-stt-webchat.d.ts +3 -3
- package/dist/src/channels/attachments/voice-stt-webchat.js +8 -14
- package/dist/src/channels/attachments/voice-stt-webchat.js.map +1 -1
- package/dist/src/channels/channel-domain.d.ts +30 -2
- package/dist/src/channels/exec-approval-runtime.d.ts +29 -0
- package/dist/src/channels/exec-approval-runtime.js +30 -0
- package/dist/src/channels/exec-approval-runtime.js.map +1 -0
- package/dist/src/channels/index.js +1 -1
- package/dist/src/channels/plugin-types.d.ts +1 -0
- package/dist/src/channels/transport-types.d.ts +2 -2
- package/dist/src/cli/bin.d.ts +1 -0
- package/dist/src/cli/bin.js +1 -0
- package/dist/src/cli/bin.js.map +1 -1
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.d.ts +2 -0
- package/dist/src/cli/commands/doctor/checks/database-schema.js +62 -0
- package/dist/src/cli/commands/doctor/checks/database-schema.js.map +1 -0
- package/dist/src/cli/commands/doctor/flow.js +2 -0
- package/dist/src/cli/commands/doctor/flow.js.map +1 -1
- package/dist/src/cli/commands/init.js +2 -57
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/onboard/channels/registry.js +1 -1
- package/dist/src/cli/commands/setup.js +6 -5
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/utils/init-workspace-core.js +1 -2
- package/dist/src/cli/utils/init-workspace-core.js.map +1 -1
- package/dist/src/config/index.js +2 -2
- package/dist/src/config/paths.d.ts +0 -13
- package/dist/src/config/paths.js +1 -20
- package/dist/src/config/paths.js.map +1 -1
- package/dist/src/config/rules.js +1 -1
- package/dist/src/config/schema.d.ts +24 -0
- package/dist/src/config/schema.js +33 -2
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/cron/executor.js +2 -2
- package/dist/src/gateway/hono/lib/config-payload.js +1 -1
- package/dist/src/gateway/hono/oauth-async.js +36 -3
- package/dist/src/gateway/hono/oauth-async.js.map +1 -1
- package/dist/src/gateway/hono/routes/config-patch/channels.js +3 -3
- package/dist/src/gateway/hono/routes/config-patch/channels.js.map +1 -1
- package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
- package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
- package/dist/src/gateway/hono/routes/media.d.ts +3 -0
- package/dist/src/gateway/hono/routes/media.js +50 -0
- package/dist/src/gateway/hono/routes/media.js.map +1 -0
- package/dist/src/gateway/hono/routes/sessions.js +6 -0
- package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +1 -87
- package/dist/src/gateway/hono/routes/workspace.js.map +1 -1
- package/dist/src/gateway/service/sessions-api.js +3 -0
- package/dist/src/gateway/service/sessions-api.js.map +1 -1
- package/dist/src/infra/http-proxy-env.d.ts +4 -0
- package/dist/src/infra/http-proxy-env.js +23 -0
- package/dist/src/infra/http-proxy-env.js.map +1 -0
- package/dist/src/media/index.d.ts +5 -0
- package/dist/src/media/index.js +5 -0
- package/dist/src/media/media-reference.d.ts +23 -0
- package/dist/src/media/media-reference.js +34 -0
- package/dist/src/media/media-reference.js.map +1 -0
- package/dist/src/media/paths.d.ts +5 -0
- package/dist/src/media/paths.js +20 -0
- package/dist/src/media/paths.js.map +1 -0
- package/dist/src/media/session-references.d.ts +8 -0
- package/dist/src/media/session-references.js +45 -0
- package/dist/src/media/session-references.js.map +1 -0
- package/dist/src/media/store.d.ts +19 -0
- package/dist/src/media/store.js +131 -0
- package/dist/src/media/store.js.map +1 -0
- package/dist/src/media/types.d.ts +34 -0
- package/dist/src/media/types.js +1 -0
- package/dist/src/media/uri.d.ts +15 -0
- package/dist/src/media/uri.js +51 -0
- package/dist/src/media/uri.js.map +1 -0
- package/dist/src/session/session-title.js +6 -3
- package/dist/src/session/session-title.js.map +1 -1
- package/dist/src/session/store.js +1 -1
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/session/types.d.ts +12 -7
- package/dist/src/session/types.js.map +1 -1
- package/dist/src/storage/sqlite/index.d.ts +4 -1
- package/dist/src/storage/sqlite/index.js +6 -2
- package/dist/src/storage/sqlite/migrations/002_add_flush_columns.sql +3 -0
- package/dist/src/storage/sqlite/migrations/discover.d.ts +4 -0
- package/dist/src/storage/sqlite/migrations/discover.js +38 -0
- package/dist/src/storage/sqlite/migrations/discover.js.map +1 -0
- package/dist/src/storage/sqlite/migrations/discover.ts +52 -0
- package/dist/src/storage/sqlite/migrations/errors.d.ts +11 -0
- package/dist/src/storage/sqlite/migrations/errors.js +27 -0
- package/dist/src/storage/sqlite/migrations/errors.js.map +1 -0
- package/dist/src/storage/sqlite/migrations/errors.ts +32 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +22 -0
- package/dist/src/storage/sqlite/migrations/runner.js +97 -0
- package/dist/src/storage/sqlite/migrations/runner.js.map +1 -0
- package/dist/src/storage/sqlite/migrations/runner.ts +153 -0
- package/dist/src/storage/sqlite/migrations/types.d.ts +12 -0
- package/dist/src/storage/sqlite/migrations/types.js +1 -0
- package/dist/src/storage/sqlite/migrations/types.ts +13 -0
- package/dist/src/storage/sqlite/row-mappers.d.ts +4 -0
- package/dist/src/storage/sqlite/row-mappers.js +4 -0
- package/dist/src/storage/sqlite/row-mappers.js.map +1 -1
- package/dist/src/storage/sqlite/schema-version.d.ts +6 -0
- package/dist/src/storage/sqlite/schema-version.js +29 -0
- package/dist/src/storage/sqlite/schema-version.js.map +1 -0
- package/dist/src/storage/sqlite/schema.d.ts +5 -3
- package/dist/src/storage/sqlite/schema.js +24 -28
- package/dist/src/storage/sqlite/schema.js.map +1 -1
- package/dist/src/storage/sqlite/schema.sql +2 -0
- package/dist/src/storage/sqlite/session-metadata.js +1 -0
- package/dist/src/storage/sqlite/session-metadata.js.map +1 -1
- package/dist/src/storage/sqlite/session-repository.js +7 -3
- package/dist/src/storage/sqlite/session-repository.js.map +1 -1
- package/package.json +3 -3
- package/dist/gateway/static/root/assets/channels-settings-BxW1Xio4.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-Cxm6AvQO.js +0 -8
- package/dist/gateway/static/root/assets/index-BJDmBCSl.css +0 -1
- package/dist/gateway/static/root/assets/index-Cbg5TS5m.js +0 -4707
- package/dist/gateway/static/root/assets/url-DpFBIyN9.js +0 -3
- package/dist/src/cli/commands/onboard/workspace.d.ts +0 -14
- package/dist/src/cli/commands/onboard/workspace.js +0 -33
- package/dist/src/cli/commands/onboard/workspace.js.map +0 -1
- package/dist/src/cli/utils/workspace.d.ts +0 -35
- package/dist/src/cli/utils/workspace.js +0 -72
- package/dist/src/cli/utils/workspace.js.map +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { init_paths_state, resolveStateDir } from "../config/paths-state.js";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
//#region src/media/paths.ts
|
|
4
|
+
init_paths_state();
|
|
5
|
+
/** Global media store root: `{stateDir}/media/`. */
|
|
6
|
+
function getMediaDir() {
|
|
7
|
+
return join(resolveStateDir(), "media");
|
|
8
|
+
}
|
|
9
|
+
function getMediaBucketDir(bucket) {
|
|
10
|
+
return join(getMediaDir(), bucket);
|
|
11
|
+
}
|
|
12
|
+
function isPathInside(root, candidate) {
|
|
13
|
+
const rootResolved = resolve(root);
|
|
14
|
+
const candidateResolved = resolve(candidate);
|
|
15
|
+
return candidateResolved === rootResolved || candidateResolved.startsWith(rootResolved + "/");
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { getMediaBucketDir, getMediaDir, isPathInside };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","names":[],"sources":["../../../src/media/paths.ts"],"sourcesContent":["import { join, resolve } from 'node:path';\n\nimport { resolveStateDir } from '../config/paths-state.js';\n\nimport type { MediaBucket } from './types.js';\n\n/** Global media store root: `{stateDir}/media/`. */\nexport function getMediaDir(): string {\n return join(resolveStateDir(), 'media');\n}\n\nexport function getMediaBucketDir(bucket: MediaBucket): string {\n return join(getMediaDir(), bucket);\n}\n\nexport function isPathInside(root: string, candidate: string): boolean {\n const rootResolved = resolve(root);\n const candidateResolved = resolve(candidate);\n return (\n candidateResolved === rootResolved ||\n candidateResolved.startsWith(rootResolved + '/')\n );\n}\n"],"mappings":";;;kBAE2D;;AAK3D,SAAgB,cAAsB;AACpC,QAAO,KAAK,iBAAiB,EAAE,QAAQ;;AAGzC,SAAgB,kBAAkB,QAA6B;AAC7D,QAAO,KAAK,aAAa,EAAE,OAAO;;AAGpC,SAAgB,aAAa,MAAc,WAA4B;CACrE,MAAM,eAAe,QAAQ,KAAK;CAClC,MAAM,oBAAoB,QAAQ,UAAU;AAC5C,QACE,sBAAsB,gBACtB,kBAAkB,WAAW,eAAe,IAAI"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentMessage } from '@earendil-works/pi-agent-core';
|
|
2
|
+
export declare function collectMediaUrisFromMessages(messages: readonly AgentMessage[]): Set<string>;
|
|
3
|
+
export declare function messagesReferenceMediaUri(messages: readonly AgentMessage[], uri: string): boolean;
|
|
4
|
+
export declare function deleteMediaUris(uris: Iterable<string>): Promise<void>;
|
|
5
|
+
export declare function deleteMediaUrisNoLongerReferenced(params: {
|
|
6
|
+
removed: readonly AgentMessage[];
|
|
7
|
+
remaining: readonly AgentMessage[];
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { tryParseMediaUri } from "./uri.js";
|
|
2
|
+
import { deleteMediaBuffer } from "./store.js";
|
|
3
|
+
//#region src/media/session-references.ts
|
|
4
|
+
function collectFromValue(value, out) {
|
|
5
|
+
if (!value) return;
|
|
6
|
+
if (typeof value === "string") {
|
|
7
|
+
if (value.startsWith("media://")) out.add(value.trim());
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const item of value) collectFromValue(item, out);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value !== "object") return;
|
|
15
|
+
const record = value;
|
|
16
|
+
if (typeof record.uri === "string" && record.uri.startsWith("media://")) out.add(record.uri.trim());
|
|
17
|
+
if (Array.isArray(record.media)) collectFromValue(record.media, out);
|
|
18
|
+
if (Array.isArray(record.attachments)) collectFromValue(record.attachments, out);
|
|
19
|
+
}
|
|
20
|
+
function collectMediaUrisFromMessages(messages) {
|
|
21
|
+
const out = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const message of messages) collectFromValue(message, out);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function messagesReferenceMediaUri(messages, uri) {
|
|
26
|
+
return collectMediaUrisFromMessages(messages).has(uri.trim());
|
|
27
|
+
}
|
|
28
|
+
async function deleteMediaUris(uris) {
|
|
29
|
+
for (const uri of new Set(uris)) {
|
|
30
|
+
const parsed = tryParseMediaUri(uri);
|
|
31
|
+
if (!parsed) continue;
|
|
32
|
+
await deleteMediaBuffer(parsed.id, parsed.bucket).catch(() => {});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function deleteMediaUrisNoLongerReferenced(params) {
|
|
36
|
+
const removedUris = collectMediaUrisFromMessages(params.removed);
|
|
37
|
+
if (removedUris.size === 0) return;
|
|
38
|
+
const remainingUris = collectMediaUrisFromMessages(params.remaining);
|
|
39
|
+
for (const uri of remainingUris) removedUris.delete(uri);
|
|
40
|
+
await deleteMediaUris(removedUris);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { collectMediaUrisFromMessages, deleteMediaUris, deleteMediaUrisNoLongerReferenced, messagesReferenceMediaUri };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=session-references.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-references.js","names":[],"sources":["../../../src/media/session-references.ts"],"sourcesContent":["import type { AgentMessage } from '@earendil-works/pi-agent-core';\n\nimport { deleteMediaBuffer } from './store.js';\nimport { tryParseMediaUri } from './uri.js';\n\nfunction collectFromValue(value: unknown, out: Set<string>): void {\n if (!value) return;\n if (typeof value === 'string') {\n if (value.startsWith('media://')) out.add(value.trim());\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) collectFromValue(item, out);\n return;\n }\n if (typeof value !== 'object') return;\n\n const record = value as Record<string, unknown>;\n if (typeof record.uri === 'string' && record.uri.startsWith('media://')) {\n out.add(record.uri.trim());\n }\n if (Array.isArray(record.media)) collectFromValue(record.media, out);\n if (Array.isArray(record.attachments)) collectFromValue(record.attachments, out);\n}\n\nexport function collectMediaUrisFromMessages(messages: readonly AgentMessage[]): Set<string> {\n const out = new Set<string>();\n for (const message of messages) collectFromValue(message, out);\n return out;\n}\n\nexport function messagesReferenceMediaUri(messages: readonly AgentMessage[], uri: string): boolean {\n return collectMediaUrisFromMessages(messages).has(uri.trim());\n}\n\nexport async function deleteMediaUris(uris: Iterable<string>): Promise<void> {\n for (const uri of new Set(uris)) {\n const parsed = tryParseMediaUri(uri);\n if (!parsed) continue;\n await deleteMediaBuffer(parsed.id, parsed.bucket).catch(() => {});\n }\n}\n\nexport async function deleteMediaUrisNoLongerReferenced(params: {\n removed: readonly AgentMessage[];\n remaining: readonly AgentMessage[];\n}): Promise<void> {\n const removedUris = collectMediaUrisFromMessages(params.removed);\n if (removedUris.size === 0) return;\n\n const remainingUris = collectMediaUrisFromMessages(params.remaining);\n for (const uri of remainingUris) removedUris.delete(uri);\n await deleteMediaUris(removedUris);\n}\n"],"mappings":";;;AAKA,SAAS,iBAAiB,OAAgB,KAAwB;AAChE,KAAI,CAAC,MAAO;AACZ,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,MAAM,WAAW,WAAW,CAAE,KAAI,IAAI,MAAM,MAAM,CAAC;AACvD;;AAEF,KAAI,MAAM,QAAQ,MAAM,EAAE;AACxB,OAAK,MAAM,QAAQ,MAAO,kBAAiB,MAAM,IAAI;AACrD;;AAEF,KAAI,OAAO,UAAU,SAAU;CAE/B,MAAM,SAAS;AACf,KAAI,OAAO,OAAO,QAAQ,YAAY,OAAO,IAAI,WAAW,WAAW,CACrE,KAAI,IAAI,OAAO,IAAI,MAAM,CAAC;AAE5B,KAAI,MAAM,QAAQ,OAAO,MAAM,CAAE,kBAAiB,OAAO,OAAO,IAAI;AACpE,KAAI,MAAM,QAAQ,OAAO,YAAY,CAAE,kBAAiB,OAAO,aAAa,IAAI;;AAGlF,SAAgB,6BAA6B,UAAgD;CAC3F,MAAM,sBAAM,IAAI,KAAa;AAC7B,MAAK,MAAM,WAAW,SAAU,kBAAiB,SAAS,IAAI;AAC9D,QAAO;;AAGT,SAAgB,0BAA0B,UAAmC,KAAsB;AACjG,QAAO,6BAA6B,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC;;AAG/D,eAAsB,gBAAgB,MAAuC;AAC3E,MAAK,MAAM,OAAO,IAAI,IAAI,KAAK,EAAE;EAC/B,MAAM,SAAS,iBAAiB,IAAI;AACpC,MAAI,CAAC,OAAQ;AACb,QAAM,kBAAkB,OAAO,IAAI,OAAO,OAAO,CAAC,YAAY,GAAG;;;AAIrE,eAAsB,kCAAkC,QAGtC;CAChB,MAAM,cAAc,6BAA6B,OAAO,QAAQ;AAChE,KAAI,YAAY,SAAS,EAAG;CAE5B,MAAM,gBAAgB,6BAA6B,OAAO,UAAU;AACpE,MAAK,MAAM,OAAO,cAAe,aAAY,OAAO,IAAI;AACxD,OAAM,gBAAgB,YAAY"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MediaBucket, SavedMedia } from './types.js';
|
|
2
|
+
/** Default per-file cap for inbound staging (5 MiB). */
|
|
3
|
+
export declare const MEDIA_MAX_BYTES: number;
|
|
4
|
+
export declare function ensureMediaDir(): Promise<string>;
|
|
5
|
+
export declare function saveMediaBuffer(buffer: Buffer, opts?: {
|
|
6
|
+
contentType?: string;
|
|
7
|
+
bucket?: MediaBucket;
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
originalFilename?: string;
|
|
10
|
+
}): Promise<SavedMedia>;
|
|
11
|
+
export declare function resolveMediaBufferPath(id: string, bucket?: MediaBucket): string;
|
|
12
|
+
export declare function readMediaBuffer(id: string, bucket?: MediaBucket, maxBytes?: number): Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
path: string;
|
|
15
|
+
buffer: Buffer;
|
|
16
|
+
size: number;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function deleteMediaBuffer(id: string, bucket?: MediaBucket): Promise<void>;
|
|
19
|
+
export declare function mimeTypeFromMediaPath(filePath: string): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getMediaBucketDir, getMediaDir, isPathInside } from "./paths.js";
|
|
2
|
+
import { buildMediaUri } from "./uri.js";
|
|
3
|
+
import { extname, join, parse } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
//#region src/media/store.ts
|
|
7
|
+
/** Default per-file cap for inbound staging (5 MiB). */
|
|
8
|
+
const MEDIA_MAX_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
const MEDIA_DIR_MODE = 448;
|
|
10
|
+
const MEDIA_FILE_MODE = 420;
|
|
11
|
+
const MIME_BY_EXT = {
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".webp": "image/webp",
|
|
17
|
+
".pdf": "application/pdf",
|
|
18
|
+
".txt": "text/plain",
|
|
19
|
+
".md": "text/markdown",
|
|
20
|
+
".json": "application/json",
|
|
21
|
+
".webm": "audio/webm",
|
|
22
|
+
".ogg": "audio/ogg",
|
|
23
|
+
".opus": "audio/ogg",
|
|
24
|
+
".mp3": "audio/mpeg",
|
|
25
|
+
".wav": "audio/wav",
|
|
26
|
+
".m4a": "audio/mp4"
|
|
27
|
+
};
|
|
28
|
+
function sanitizeFilename(name) {
|
|
29
|
+
return (name.replace(/[/\\?%*:|"<>]/g, "_").trim() || "file").slice(0, 60);
|
|
30
|
+
}
|
|
31
|
+
function extFromMime(mimeType, fallbackName) {
|
|
32
|
+
if (mimeType) {
|
|
33
|
+
const m = mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
34
|
+
if (m === "image/jpeg") return ".jpg";
|
|
35
|
+
if (m === "image/png") return ".png";
|
|
36
|
+
if (m === "image/webp") return ".webp";
|
|
37
|
+
if (m === "image/gif") return ".gif";
|
|
38
|
+
if (m === "application/pdf") return ".pdf";
|
|
39
|
+
if (m === "audio/ogg") return ".ogg";
|
|
40
|
+
if (m === "audio/mpeg") return ".mp3";
|
|
41
|
+
if (m === "audio/wav") return ".wav";
|
|
42
|
+
if (m === "audio/webm") return ".webm";
|
|
43
|
+
if (m === "audio/mp4") return ".m4a";
|
|
44
|
+
if (m === "text/plain") return ".txt";
|
|
45
|
+
}
|
|
46
|
+
const fromName = fallbackName ? extname(fallbackName).toLowerCase() : "";
|
|
47
|
+
if (fromName && fromName.length <= 10) return fromName;
|
|
48
|
+
return ".bin";
|
|
49
|
+
}
|
|
50
|
+
function buildSavedMediaId(opts) {
|
|
51
|
+
const ext = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`;
|
|
52
|
+
if (opts.originalFilename) {
|
|
53
|
+
const stem = sanitizeFilename(parse(opts.originalFilename).name);
|
|
54
|
+
if (stem) return `${stem}---${opts.baseId}${ext}`;
|
|
55
|
+
}
|
|
56
|
+
return `${opts.baseId}${ext}`;
|
|
57
|
+
}
|
|
58
|
+
function assertSafeMediaId(id, caller) {
|
|
59
|
+
if (!id || id.includes("/") || id.includes("\\") || id.includes("\0") || id === "..") throw new Error(`${caller}: unsafe media id: ${JSON.stringify(id)}`);
|
|
60
|
+
}
|
|
61
|
+
function assertSafeBucket(bucket) {
|
|
62
|
+
if (bucket !== "inbound" && bucket !== "tts" && bucket !== "outbound") throw new Error(`Unsupported media bucket: ${JSON.stringify(bucket)}`);
|
|
63
|
+
}
|
|
64
|
+
async function ensureMediaDir() {
|
|
65
|
+
const root = getMediaDir();
|
|
66
|
+
await mkdir(root, {
|
|
67
|
+
recursive: true,
|
|
68
|
+
mode: MEDIA_DIR_MODE
|
|
69
|
+
});
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
72
|
+
async function saveMediaBuffer(buffer, opts) {
|
|
73
|
+
const bucket = opts?.bucket ?? "inbound";
|
|
74
|
+
const maxBytes = opts?.maxBytes ?? 5242880;
|
|
75
|
+
if (buffer.byteLength > maxBytes) throw new Error(`Media exceeds ${Math.floor(maxBytes / (1024 * 1024))}MB limit`);
|
|
76
|
+
await ensureMediaDir();
|
|
77
|
+
const dir = getMediaBucketDir(bucket);
|
|
78
|
+
await mkdir(dir, {
|
|
79
|
+
recursive: true,
|
|
80
|
+
mode: MEDIA_DIR_MODE
|
|
81
|
+
});
|
|
82
|
+
const baseId = randomUUID();
|
|
83
|
+
const ext = extFromMime(opts?.contentType, opts?.originalFilename);
|
|
84
|
+
const id = buildSavedMediaId({
|
|
85
|
+
baseId,
|
|
86
|
+
ext,
|
|
87
|
+
originalFilename: opts?.originalFilename
|
|
88
|
+
});
|
|
89
|
+
assertSafeMediaId(id, "saveMediaBuffer");
|
|
90
|
+
const absPath = join(dir, id);
|
|
91
|
+
if (!isPathInside(dir, absPath)) throw new Error("Media path escapes bucket directory");
|
|
92
|
+
await writeFile(absPath, buffer, { mode: MEDIA_FILE_MODE });
|
|
93
|
+
const uri = buildMediaUri(bucket, id);
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
path: absPath,
|
|
97
|
+
size: buffer.byteLength,
|
|
98
|
+
contentType: opts?.contentType?.split(";")[0]?.trim() || MIME_BY_EXT[ext] || "application/octet-stream",
|
|
99
|
+
bucket,
|
|
100
|
+
uri
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolveMediaBufferPath(id, bucket = "inbound") {
|
|
104
|
+
assertSafeBucket(bucket);
|
|
105
|
+
assertSafeMediaId(id, "resolveMediaBufferPath");
|
|
106
|
+
const dir = getMediaBucketDir(bucket);
|
|
107
|
+
const abs = join(dir, id);
|
|
108
|
+
if (!isPathInside(dir, abs)) throw new Error("Media path escapes bucket directory");
|
|
109
|
+
return abs;
|
|
110
|
+
}
|
|
111
|
+
async function readMediaBuffer(id, bucket = "inbound", maxBytes = MEDIA_MAX_BYTES) {
|
|
112
|
+
const path = resolveMediaBufferPath(id, bucket);
|
|
113
|
+
const buffer = await readFile(path);
|
|
114
|
+
if (buffer.byteLength > maxBytes) throw new Error(`Media ${JSON.stringify(id)} exceeds read limit`);
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
path,
|
|
118
|
+
buffer,
|
|
119
|
+
size: buffer.byteLength
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function deleteMediaBuffer(id, bucket = "inbound") {
|
|
123
|
+
await rm(resolveMediaBufferPath(id, bucket), { force: true });
|
|
124
|
+
}
|
|
125
|
+
function mimeTypeFromMediaPath(filePath) {
|
|
126
|
+
return MIME_BY_EXT[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { MEDIA_MAX_BYTES, deleteMediaBuffer, ensureMediaDir, mimeTypeFromMediaPath, readMediaBuffer, resolveMediaBufferPath, saveMediaBuffer };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":["parsePath"],"sources":["../../../src/media/store.ts"],"sourcesContent":["import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { extname, join, parse as parsePath } from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nimport { getMediaBucketDir, getMediaDir, isPathInside } from './paths.js';\nimport { buildMediaUri } from './uri.js';\nimport type { MediaBucket, SavedMedia } from './types.js';\n\n/** Default per-file cap for inbound staging (5 MiB). */\nexport const MEDIA_MAX_BYTES = 5 * 1024 * 1024;\n\nconst MEDIA_DIR_MODE = 0o700;\nconst MEDIA_FILE_MODE = 0o644;\n\nconst MIME_BY_EXT: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.pdf': 'application/pdf',\n '.txt': 'text/plain',\n '.md': 'text/markdown',\n '.json': 'application/json',\n '.webm': 'audio/webm',\n '.ogg': 'audio/ogg',\n '.opus': 'audio/ogg',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.m4a': 'audio/mp4',\n};\n\nfunction sanitizeFilename(name: string): string {\n const base = name.replace(/[/\\\\?%*:|\"<>]/g, '_').trim() || 'file';\n return base.slice(0, 60);\n}\n\nfunction extFromMime(mimeType: string | undefined, fallbackName?: string): string {\n if (mimeType) {\n const m = mimeType.split(';')[0]?.trim().toLowerCase() ?? '';\n if (m === 'image/jpeg') return '.jpg';\n if (m === 'image/png') return '.png';\n if (m === 'image/webp') return '.webp';\n if (m === 'image/gif') return '.gif';\n if (m === 'application/pdf') return '.pdf';\n if (m === 'audio/ogg') return '.ogg';\n if (m === 'audio/mpeg') return '.mp3';\n if (m === 'audio/wav') return '.wav';\n if (m === 'audio/webm') return '.webm';\n if (m === 'audio/mp4') return '.m4a';\n if (m === 'text/plain') return '.txt';\n }\n const fromName = fallbackName ? extname(fallbackName).toLowerCase() : '';\n if (fromName && fromName.length <= 10) return fromName;\n return '.bin';\n}\n\nfunction buildSavedMediaId(opts: {\n baseId: string;\n ext: string;\n originalFilename?: string;\n}): string {\n const ext = opts.ext.startsWith('.') ? opts.ext : `.${opts.ext}`;\n if (opts.originalFilename) {\n const stem = sanitizeFilename(parsePath(opts.originalFilename).name);\n if (stem) {\n return `${stem}---${opts.baseId}${ext}`;\n }\n }\n return `${opts.baseId}${ext}`;\n}\n\nfunction assertSafeMediaId(id: string, caller: string): void {\n if (!id || id.includes('/') || id.includes('\\\\') || id.includes('\\0') || id === '..') {\n throw new Error(`${caller}: unsafe media id: ${JSON.stringify(id)}`);\n }\n}\n\nfunction assertSafeBucket(bucket: string): asserts bucket is MediaBucket {\n if (bucket !== 'inbound' && bucket !== 'tts' && bucket !== 'outbound') {\n throw new Error(`Unsupported media bucket: ${JSON.stringify(bucket)}`);\n }\n}\n\nexport async function ensureMediaDir(): Promise<string> {\n const root = getMediaDir();\n await mkdir(root, { recursive: true, mode: MEDIA_DIR_MODE });\n return root;\n}\n\nexport async function saveMediaBuffer(\n buffer: Buffer,\n opts?: {\n contentType?: string;\n bucket?: MediaBucket;\n maxBytes?: number;\n originalFilename?: string;\n },\n): Promise<SavedMedia> {\n const bucket = opts?.bucket ?? 'inbound';\n const maxBytes = opts?.maxBytes ?? MEDIA_MAX_BYTES;\n if (buffer.byteLength > maxBytes) {\n throw new Error(`Media exceeds ${Math.floor(maxBytes / (1024 * 1024))}MB limit`);\n }\n\n await ensureMediaDir();\n const dir = getMediaBucketDir(bucket);\n await mkdir(dir, { recursive: true, mode: MEDIA_DIR_MODE });\n\n const baseId = randomUUID();\n const ext = extFromMime(opts?.contentType, opts?.originalFilename);\n const id = buildSavedMediaId({ baseId, ext, originalFilename: opts?.originalFilename });\n assertSafeMediaId(id, 'saveMediaBuffer');\n\n const absPath = join(dir, id);\n if (!isPathInside(dir, absPath)) {\n throw new Error('Media path escapes bucket directory');\n }\n\n await writeFile(absPath, buffer, { mode: MEDIA_FILE_MODE });\n const uri = buildMediaUri(bucket, id);\n\n return {\n id,\n path: absPath,\n size: buffer.byteLength,\n contentType: opts?.contentType?.split(';')[0]?.trim() || MIME_BY_EXT[ext] || 'application/octet-stream',\n bucket,\n uri,\n };\n}\n\nexport function resolveMediaBufferPath(id: string, bucket: MediaBucket = 'inbound'): string {\n assertSafeBucket(bucket);\n assertSafeMediaId(id, 'resolveMediaBufferPath');\n const dir = getMediaBucketDir(bucket);\n const abs = join(dir, id);\n if (!isPathInside(dir, abs)) {\n throw new Error('Media path escapes bucket directory');\n }\n return abs;\n}\n\nexport async function readMediaBuffer(\n id: string,\n bucket: MediaBucket = 'inbound',\n maxBytes = MEDIA_MAX_BYTES,\n): Promise<{ id: string; path: string; buffer: Buffer; size: number }> {\n const path = resolveMediaBufferPath(id, bucket);\n const buffer = await readFile(path);\n if (buffer.byteLength > maxBytes) {\n throw new Error(`Media ${JSON.stringify(id)} exceeds read limit`);\n }\n return { id, path, buffer, size: buffer.byteLength };\n}\n\nexport async function deleteMediaBuffer(id: string, bucket: MediaBucket = 'inbound'): Promise<void> {\n const path = resolveMediaBufferPath(id, bucket);\n await rm(path, { force: true });\n}\n\nexport function mimeTypeFromMediaPath(filePath: string): string {\n const ext = extname(filePath).toLowerCase();\n return MIME_BY_EXT[ext] ?? 'application/octet-stream';\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Media store bucket — maps to `{stateDir}/media/<bucket>/`. */
|
|
2
|
+
export type MediaBucket = 'inbound' | 'tts' | 'outbound';
|
|
3
|
+
/** Persisted media reference — never includes base64. */
|
|
4
|
+
export interface MediaRef {
|
|
5
|
+
id: string;
|
|
6
|
+
bucket: MediaBucket;
|
|
7
|
+
type: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
name: string;
|
|
10
|
+
size: number;
|
|
11
|
+
/** Canonical URI, e.g. `media://inbound/photo---uuid.png`. */
|
|
12
|
+
uri: string;
|
|
13
|
+
/** Absolute filesystem path (for MediaPaths transcript fields). */
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
export type SavedMedia = {
|
|
17
|
+
id: string;
|
|
18
|
+
path: string;
|
|
19
|
+
size: number;
|
|
20
|
+
contentType: string;
|
|
21
|
+
bucket: MediaBucket;
|
|
22
|
+
uri: string;
|
|
23
|
+
};
|
|
24
|
+
/** Wire / pre-persist attachment (may include base64 `data`). */
|
|
25
|
+
export interface InboundAttachmentInput {
|
|
26
|
+
id?: string;
|
|
27
|
+
type: string;
|
|
28
|
+
mimeType?: string;
|
|
29
|
+
data?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
size?: number;
|
|
32
|
+
/** Already persisted — pass-through by URI. */
|
|
33
|
+
uri?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MediaBucket } from './types.js';
|
|
2
|
+
export declare class MediaUriError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function buildMediaUri(bucket: MediaBucket, id: string): string;
|
|
6
|
+
export declare function parseMediaUri(source: string): {
|
|
7
|
+
bucket: MediaBucket;
|
|
8
|
+
id: string;
|
|
9
|
+
uri: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function tryParseMediaUri(source: string): {
|
|
12
|
+
bucket: MediaBucket;
|
|
13
|
+
id: string;
|
|
14
|
+
uri: string;
|
|
15
|
+
} | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/media/uri.ts
|
|
2
|
+
const BUCKETS = new Set([
|
|
3
|
+
"inbound",
|
|
4
|
+
"tts",
|
|
5
|
+
"outbound"
|
|
6
|
+
]);
|
|
7
|
+
var MediaUriError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "MediaUriError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function buildMediaUri(bucket, id) {
|
|
14
|
+
if (!id || id.includes("/") || id.includes("\\") || id.includes("\0")) throw new MediaUriError(`Invalid media id: ${JSON.stringify(id)}`);
|
|
15
|
+
return `media://${bucket}/${encodeURIComponent(id).replace(/%2F/g, "/")}`;
|
|
16
|
+
}
|
|
17
|
+
function parseMediaUri(source) {
|
|
18
|
+
const trimmed = source.trim();
|
|
19
|
+
if (!/^media:\/\//i.test(trimmed)) throw new MediaUriError(`Not a media URI: ${JSON.stringify(source)}`);
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(trimmed);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new MediaUriError(`Invalid media URI: ${trimmed}`);
|
|
25
|
+
}
|
|
26
|
+
const bucket = parsed.hostname;
|
|
27
|
+
if (!BUCKETS.has(bucket)) throw new MediaUriError(`Unsupported media bucket: ${bucket || "(missing)"}`);
|
|
28
|
+
let id;
|
|
29
|
+
try {
|
|
30
|
+
id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
31
|
+
} catch {
|
|
32
|
+
throw new MediaUriError(`Invalid media URI: ${trimmed}`);
|
|
33
|
+
}
|
|
34
|
+
if (!id || id.includes("/") || id.includes("\\") || id.includes("\0")) throw new MediaUriError(`Invalid media URI id: ${trimmed}`);
|
|
35
|
+
return {
|
|
36
|
+
bucket,
|
|
37
|
+
id,
|
|
38
|
+
uri: trimmed
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function tryParseMediaUri(source) {
|
|
42
|
+
try {
|
|
43
|
+
return parseMediaUri(source);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { MediaUriError, buildMediaUri, parseMediaUri, tryParseMediaUri };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=uri.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uri.js","names":[],"sources":["../../../src/media/uri.ts"],"sourcesContent":["import type { MediaBucket } from './types.js';\n\nconst BUCKETS: ReadonlySet<string> = new Set(['inbound', 'tts', 'outbound']);\n\nexport class MediaUriError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'MediaUriError';\n }\n}\n\nexport function buildMediaUri(bucket: MediaBucket, id: string): string {\n if (!id || id.includes('/') || id.includes('\\\\') || id.includes('\\0')) {\n throw new MediaUriError(`Invalid media id: ${JSON.stringify(id)}`);\n }\n return `media://${bucket}/${encodeURIComponent(id).replace(/%2F/g, '/')}`;\n}\n\nexport function parseMediaUri(source: string): { bucket: MediaBucket; id: string; uri: string } {\n const trimmed = source.trim();\n if (!/^media:\\/\\//i.test(trimmed)) {\n throw new MediaUriError(`Not a media URI: ${JSON.stringify(source)}`);\n }\n\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new MediaUriError(`Invalid media URI: ${trimmed}`);\n }\n\n const bucket = parsed.hostname;\n if (!BUCKETS.has(bucket)) {\n throw new MediaUriError(`Unsupported media bucket: ${bucket || '(missing)'}`);\n }\n\n let id: string;\n try {\n id = decodeURIComponent(parsed.pathname.replace(/^\\/+/, ''));\n } catch {\n throw new MediaUriError(`Invalid media URI: ${trimmed}`);\n }\n\n if (!id || id.includes('/') || id.includes('\\\\') || id.includes('\\0')) {\n throw new MediaUriError(`Invalid media URI id: ${trimmed}`);\n }\n\n return {\n bucket: bucket as MediaBucket,\n id,\n uri: trimmed,\n };\n}\n\nexport function tryParseMediaUri(source: string): { bucket: MediaBucket; id: string; uri: string } | null {\n try {\n return parseMediaUri(source);\n } catch {\n return null;\n }\n}\n"],"mappings":";AAEA,MAAM,UAA+B,IAAI,IAAI;CAAC;CAAW;CAAO;CAAW,CAAC;AAE5E,IAAa,gBAAb,cAAmC,MAAM;CACvC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAIhB,SAAgB,cAAc,QAAqB,IAAoB;AACrE,KAAI,CAAC,MAAM,GAAG,SAAS,IAAI,IAAI,GAAG,SAAS,KAAK,IAAI,GAAG,SAAS,KAAK,CACnE,OAAM,IAAI,cAAc,qBAAqB,KAAK,UAAU,GAAG,GAAG;AAEpE,QAAO,WAAW,OAAO,GAAG,mBAAmB,GAAG,CAAC,QAAQ,QAAQ,IAAI;;AAGzE,SAAgB,cAAc,QAAkE;CAC9F,MAAM,UAAU,OAAO,MAAM;AAC7B,KAAI,CAAC,eAAe,KAAK,QAAQ,CAC/B,OAAM,IAAI,cAAc,oBAAoB,KAAK,UAAU,OAAO,GAAG;CAGvE,IAAI;AACJ,KAAI;AACF,WAAS,IAAI,IAAI,QAAQ;SACnB;AACN,QAAM,IAAI,cAAc,sBAAsB,UAAU;;CAG1D,MAAM,SAAS,OAAO;AACtB,KAAI,CAAC,QAAQ,IAAI,OAAO,CACtB,OAAM,IAAI,cAAc,6BAA6B,UAAU,cAAc;CAG/E,IAAI;AACJ,KAAI;AACF,OAAK,mBAAmB,OAAO,SAAS,QAAQ,QAAQ,GAAG,CAAC;SACtD;AACN,QAAM,IAAI,cAAc,sBAAsB,UAAU;;AAG1D,KAAI,CAAC,MAAM,GAAG,SAAS,IAAI,IAAI,GAAG,SAAS,KAAK,IAAI,GAAG,SAAS,KAAK,CACnE,OAAM,IAAI,cAAc,yBAAyB,UAAU;AAG7D,QAAO;EACG;EACR;EACA,KAAK;EACN;;AAGH,SAAgB,iBAAiB,QAAyE;AACxG,KAAI;AACF,SAAO,cAAc,OAAO;SACtB;AACN,SAAO"}
|
|
@@ -5,7 +5,6 @@ import { init_logger } from "../utils/logger.js";
|
|
|
5
5
|
import { init_providers, resolveModel } from "../providers/index.js";
|
|
6
6
|
import { readAgentMessageContent } from "../agent/memory/agent-message-access.js";
|
|
7
7
|
import { stripSessionStartupContextFromUserText } from "../agent/reply/startup-context.js";
|
|
8
|
-
import { stripInboundFileMetadataFromText } from "../channels/attachments/inbound-persist.js";
|
|
9
8
|
import { stripEnvelopeTimestampPrefix } from "../channels/envelope-timestamp.js";
|
|
10
9
|
import { complete } from "@earendil-works/pi-ai";
|
|
11
10
|
//#region src/session/session-title.ts
|
|
@@ -33,7 +32,11 @@ function extractTextFromMessage(m) {
|
|
|
33
32
|
function firstUserText(messages) {
|
|
34
33
|
const u = messages.find((m) => m.role === "user");
|
|
35
34
|
if (!u) return "";
|
|
36
|
-
return
|
|
35
|
+
return stripMediaClaimChecks(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(extractTextFromMessage(u))));
|
|
36
|
+
}
|
|
37
|
+
function stripMediaClaimChecks(text) {
|
|
38
|
+
if (!text.includes("[media attached:") && !text.includes("xopc-media-uri:")) return text;
|
|
39
|
+
return text.replace(/\s*\[media attached:[^\]]+\]\s*\r?\nxopc-media-uri:[^\r\n]+\r?\n\s*xopc-media-path:[^\r\n]+(?:\r?\n\s*Use the read_media tool[^\r\n]*)?/g, "").replace(/\s*\[media attached:[^\]]+\]\s*/g, " ").replace(/\s*xopc-media-uri:[^\r\n]+/g, "").replace(/\s*xopc-media-path:[^\r\n]+/g, "").replace(/\s*Use the read_media tool[^\r\n]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
37
40
|
}
|
|
38
41
|
/** First assistant message that has visible text (skips tool-only assistant rows). */
|
|
39
42
|
function firstAssistantText(messages) {
|
|
@@ -70,7 +73,7 @@ function getSessionTitleSource(meta) {
|
|
|
70
73
|
}
|
|
71
74
|
/** Title from a single user message (first line), for immediate sidebar labels. */
|
|
72
75
|
function provisionalTitleFromUserText(raw) {
|
|
73
|
-
const text =
|
|
76
|
+
const text = stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText((raw ?? "").trim()));
|
|
74
77
|
if (!text) return null;
|
|
75
78
|
const line = text.split(/\n/)[0]?.trim();
|
|
76
79
|
if (!line) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripInboundFileMetadataFromText } from '../channels/attachments/inbound-persist.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n // User turns include `formatInboundFileTextBlock` text blocks; do not feed [File:…] into title LLM / fallback.\n // Inbound pipeline / webchat prepends `[YYYY-MM-DD HH:MM TZ]`; strip so titles are not timestamp-led.\n return stripInboundFileMetadataFromText(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)),\n );\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\nexport type SessionTitleSource = 'provisional' | 'llm' | 'user';\n\nexport function getSessionTitleSource(\n meta: { customData?: Record<string, unknown> } | null | undefined,\n): SessionTitleSource | null {\n const raw = meta?.customData?.titleSource;\n if (raw === 'provisional' || raw === 'llm' || raw === 'user') return raw;\n return null;\n}\n\n/** Title from a single user message (first line), for immediate sidebar labels. */\nexport function provisionalTitleFromUserText(raw: string): string | null {\n const text = stripInboundFileMetadataFromText(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText((raw ?? '').trim())),\n );\n if (!text) return null;\n const line = text.split(/\\n/)[0]?.trim();\n if (!line) return null;\n return sanitizeSessionTitle(line);\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\nexport type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;\n\n/** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */\nexport function shouldRefineSessionTitleWithLlm(\n meta: { name?: string; customData?: Record<string, unknown> } | null | undefined,\n): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n if (meta.name?.trim()) return source === 'provisional';\n return true;\n}\n\nfunction canAutoWriteTitle(meta: { name?: string; customData?: Record<string, unknown> } | null): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n return !meta.name?.trim();\n}\n\n/**\n * Set provisional title from first user text when session is still unnamed.\n * Skips cron/heartbeat keys and user-locked titles.\n */\nexport async function maybeSetProvisionalSessionTitle(\n sessionStore: SessionStore,\n sessionKey: string,\n userText?: string,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) return;\n if (!canAutoWriteTitle(meta)) return;\n\n let title: string | null = null;\n if (userText?.trim()) {\n title = provisionalTitleFromUserText(userText);\n }\n if (!title) {\n const messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'provisional' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: provisional updateMetadata failed');\n }\n}\n\n/**\n * LLM refine when title is empty or still provisional (not user-locked).\n */\nexport async function maybeRefineSessionTitleWithLlm(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (!shouldRefineSessionTitleWithLlm(meta)) return;\n\n const source = getSessionTitleSource(meta);\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n const existing = meta.name?.trim();\n if (existing === title) {\n if (source !== 'llm') {\n try {\n await sessionStore.updateMetadata(sessionKey, {\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n } catch {\n /* ignore */\n }\n }\n return;\n }\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: refine updateMetadata failed');\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n return stripMediaClaimChecks(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)));\n}\n\nfunction stripMediaClaimChecks(text: string): string {\n if (!text.includes('[media attached:') && !text.includes('xopc-media-uri:')) {\n return text;\n }\n return text\n .replace(\n /\\s*\\[media attached:[^\\]]+\\]\\s*\\r?\\nxopc-media-uri:[^\\r\\n]+\\r?\\n\\s*xopc-media-path:[^\\r\\n]+(?:\\r?\\n\\s*Use the read_media tool[^\\r\\n]*)?/g,\n '',\n )\n .replace(/\\s*\\[media attached:[^\\]]+\\]\\s*/g, ' ')\n .replace(/\\s*xopc-media-uri:[^\\r\\n]+/g, '')\n .replace(/\\s*xopc-media-path:[^\\r\\n]+/g, '')\n .replace(/\\s*Use the read_media tool[^\\r\\n]*/g, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\nexport type SessionTitleSource = 'provisional' | 'llm' | 'user';\n\nexport function getSessionTitleSource(\n meta: { customData?: Record<string, unknown> } | null | undefined,\n): SessionTitleSource | null {\n const raw = meta?.customData?.titleSource;\n if (raw === 'provisional' || raw === 'llm' || raw === 'user') return raw;\n return null;\n}\n\n/** Title from a single user message (first line), for immediate sidebar labels. */\nexport function provisionalTitleFromUserText(raw: string): string | null {\n const text = stripEnvelopeTimestampPrefix(\n stripSessionStartupContextFromUserText((raw ?? '').trim()),\n );\n if (!text) return null;\n const line = text.split(/\\n/)[0]?.trim();\n if (!line) return null;\n return sanitizeSessionTitle(line);\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\nexport type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;\n\n/** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */\nexport function shouldRefineSessionTitleWithLlm(\n meta: { name?: string; customData?: Record<string, unknown> } | null | undefined,\n): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n if (meta.name?.trim()) return source === 'provisional';\n return true;\n}\n\nfunction canAutoWriteTitle(meta: { name?: string; customData?: Record<string, unknown> } | null): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n return !meta.name?.trim();\n}\n\n/**\n * Set provisional title from first user text when session is still unnamed.\n * Skips cron/heartbeat keys and user-locked titles.\n */\nexport async function maybeSetProvisionalSessionTitle(\n sessionStore: SessionStore,\n sessionKey: string,\n userText?: string,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) return;\n if (!canAutoWriteTitle(meta)) return;\n\n let title: string | null = null;\n if (userText?.trim()) {\n title = provisionalTitleFromUserText(userText);\n }\n if (!title) {\n const messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'provisional' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: provisional updateMetadata failed');\n }\n}\n\n/**\n * LLM refine when title is empty or still provisional (not user-locked).\n */\nexport async function maybeRefineSessionTitleWithLlm(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (!shouldRefineSessionTitleWithLlm(meta)) return;\n\n const source = getSessionTitleSource(meta);\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n const existing = meta.name?.trim();\n if (existing === title) {\n if (source !== 'llm') {\n try {\n await sessionStore.updateMetadata(sessionKey, {\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n } catch {\n /* ignore */\n }\n }\n return;\n }\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: refine updateMetadata failed');\n }\n}\n"],"mappings":"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"}
|
|
@@ -480,7 +480,7 @@ var SessionStore = class {
|
|
|
480
480
|
tool_calls: m.tool_calls,
|
|
481
481
|
name: m.name
|
|
482
482
|
};
|
|
483
|
-
if (Array.isArray(m.
|
|
483
|
+
if (Array.isArray(m.media) && m.media.length > 0) row.media = m.media;
|
|
484
484
|
const rawUsage = m.usage;
|
|
485
485
|
if (rawUsage && typeof rawUsage === "object") {
|
|
486
486
|
const inputTokens = typeof rawUsage.input === "number" ? rawUsage.input : void 0;
|