@jsbot-tellus/core 2.3.0 → 2.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.
Files changed (110) hide show
  1. package/README.md +19 -0
  2. package/bot.config.example.json +3 -0
  3. package/dist/app.js +215 -98
  4. package/dist/app.js.map +1 -1
  5. package/dist/core/client.d.ts +1 -0
  6. package/dist/core/client.d.ts.map +1 -1
  7. package/dist/core/client.js +3 -0
  8. package/dist/core/client.js.map +1 -1
  9. package/dist/core/config/schema.d.ts +19 -0
  10. package/dist/core/config/schema.d.ts.map +1 -1
  11. package/dist/core/config/schema.js +37 -0
  12. package/dist/core/config/schema.js.map +1 -1
  13. package/dist/core/dispatch/router.d.ts +3 -0
  14. package/dist/core/dispatch/router.d.ts.map +1 -1
  15. package/dist/core/dispatch/router.js +13 -0
  16. package/dist/core/dispatch/router.js.map +1 -1
  17. package/dist/core/index.d.ts +2 -2
  18. package/dist/core/index.d.ts.map +1 -1
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/kernel.d.ts +8 -0
  21. package/dist/core/kernel.d.ts.map +1 -1
  22. package/dist/core/kernel.js +17 -0
  23. package/dist/core/kernel.js.map +1 -1
  24. package/dist/core/logging/log-buffer.d.ts +27 -0
  25. package/dist/core/logging/log-buffer.d.ts.map +1 -0
  26. package/dist/core/logging/log-buffer.js +71 -0
  27. package/dist/core/logging/log-buffer.js.map +1 -0
  28. package/dist/core/logging/logger.d.ts +3 -0
  29. package/dist/core/logging/logger.d.ts.map +1 -1
  30. package/dist/core/logging/logger.js +6 -3
  31. package/dist/core/logging/logger.js.map +1 -1
  32. package/dist/core/storage/session.d.ts +15 -0
  33. package/dist/core/storage/session.d.ts.map +1 -1
  34. package/dist/core/storage/session.js +55 -0
  35. package/dist/core/storage/session.js.map +1 -1
  36. package/dist/core/web/api.d.ts +16 -0
  37. package/dist/core/web/api.d.ts.map +1 -0
  38. package/dist/core/web/api.js +617 -0
  39. package/dist/core/web/api.js.map +1 -0
  40. package/dist/core/web/auth.d.ts +43 -0
  41. package/dist/core/web/auth.d.ts.map +1 -0
  42. package/dist/core/web/auth.js +161 -0
  43. package/dist/core/web/auth.js.map +1 -0
  44. package/dist/core/web/config-store.d.ts +30 -0
  45. package/dist/core/web/config-store.d.ts.map +1 -0
  46. package/dist/core/web/config-store.js +216 -0
  47. package/dist/core/web/config-store.js.map +1 -0
  48. package/dist/core/web/event-feed.d.ts +26 -0
  49. package/dist/core/web/event-feed.d.ts.map +1 -0
  50. package/dist/core/web/event-feed.js +107 -0
  51. package/dist/core/web/event-feed.js.map +1 -0
  52. package/dist/core/web/http.d.ts +39 -0
  53. package/dist/core/web/http.d.ts.map +1 -0
  54. package/dist/core/web/http.js +233 -0
  55. package/dist/core/web/http.js.map +1 -0
  56. package/dist/core/web/index.d.ts +11 -0
  57. package/dist/core/web/index.d.ts.map +1 -0
  58. package/dist/core/web/index.js +6 -0
  59. package/dist/core/web/index.js.map +1 -0
  60. package/dist/core/web/server.d.ts +30 -0
  61. package/dist/core/web/server.d.ts.map +1 -0
  62. package/dist/core/web/server.js +437 -0
  63. package/dist/core/web/server.js.map +1 -0
  64. package/dist/core/web/types.d.ts +84 -0
  65. package/dist/core/web/types.d.ts.map +1 -0
  66. package/dist/core/web/types.js +2 -0
  67. package/dist/core/web/types.js.map +1 -0
  68. package/dist/webui/assets/ActionsView-BDuXBWG1.js +1 -0
  69. package/dist/webui/assets/ActionsView-Ciib5lC6.css +1 -0
  70. package/dist/webui/assets/ConfigView-B3Gv2uRG.js +1 -0
  71. package/dist/webui/assets/ConfigView-CrEpss2y.css +1 -0
  72. package/dist/webui/assets/DashboardView-DR6yCaqs.css +1 -0
  73. package/dist/webui/assets/DashboardView-n9LGPmQL.js +1 -0
  74. package/dist/webui/assets/EventsView-DhO8twWb.js +1 -0
  75. package/dist/webui/assets/EventsView-PMkYKxZ-.css +1 -0
  76. package/dist/webui/assets/KvView-BCz_dOVC.js +1 -0
  77. package/dist/webui/assets/KvView-CaMvbxUz.css +1 -0
  78. package/dist/webui/assets/LoginView-BVnFkwSW.css +1 -0
  79. package/dist/webui/assets/LoginView-Br3_AC5z.js +1 -0
  80. package/dist/webui/assets/LogsView-Ba_12frI.js +1 -0
  81. package/dist/webui/assets/LogsView-NR4L1-XY.css +1 -0
  82. package/dist/webui/assets/MainLayout-D2LTc57u.js +1 -0
  83. package/dist/webui/assets/MainLayout-glfVNbMz.css +1 -0
  84. package/dist/webui/assets/PluginsView-DL9OxGph.css +1 -0
  85. package/dist/webui/assets/PluginsView-vQKBSCU8.js +1 -0
  86. package/dist/webui/assets/SessionsView-DSl37qpo.js +1 -0
  87. package/dist/webui/assets/SessionsView-p-u3JGTV.css +1 -0
  88. package/dist/webui/assets/api-CNzxhTWe.js +3 -0
  89. package/dist/webui/assets/index-CUvpnClx.js +50 -0
  90. package/dist/webui/assets/index-CePlRZX6.css +1 -0
  91. package/dist/webui/index.html +110 -0
  92. package/docs/webui.md +142 -0
  93. package/package.json +14 -3
  94. package/src/app.ts +286 -118
  95. package/src/core/client.ts +4 -0
  96. package/src/core/config/schema.ts +40 -0
  97. package/src/core/dispatch/router.ts +15 -0
  98. package/src/core/index.ts +3 -0
  99. package/src/core/kernel.ts +25 -1
  100. package/src/core/logging/log-buffer.ts +109 -0
  101. package/src/core/logging/logger.ts +7 -3
  102. package/src/core/storage/session.ts +95 -0
  103. package/src/core/web/api.ts +932 -0
  104. package/src/core/web/auth.ts +245 -0
  105. package/src/core/web/config-store.ts +333 -0
  106. package/src/core/web/event-feed.ts +163 -0
  107. package/src/core/web/http.ts +340 -0
  108. package/src/core/web/index.ts +25 -0
  109. package/src/core/web/server.ts +621 -0
  110. package/src/core/web/types.ts +100 -0
@@ -1,6 +1,7 @@
1
+ import type { SnowLumaEvent } from '@snowluma/sdk'
1
2
  import type { BotClient } from './client.js'
2
3
  import type { AppConfig } from './config/schema.js'
3
- import { createBotApi } from './api.js'
4
+ import { createBotApi, type BotApi } from './api.js'
4
5
  import { Router } from './dispatch/router.js'
5
6
  import { PerKeyTaskQueue } from './dispatch/task-queue.js'
6
7
  import { Lifecycle, type LifecycleState } from './lifecycle/lifecycle.js'
@@ -28,10 +29,13 @@ export interface KernelOptions {
28
29
  plugins?: AnyPluginDefinition[] | undefined
29
30
  installer?: PluginInstaller | undefined
30
31
  onRestart?: (() => void) | undefined
32
+ onEvent?: ((event: SnowLumaEvent) => void) | undefined
31
33
  drainTimeoutMs?: number | undefined
32
34
  }
33
35
 
34
36
  export class Kernel {
37
+ readonly api: BotApi
38
+
35
39
  private readonly config: AppConfig
36
40
  private readonly client: BotClient
37
41
  private readonly logger: Logger
@@ -62,12 +66,16 @@ export class Kernel {
62
66
  const tasks = new PerKeyTaskQueue(options.logger)
63
67
  const api = createBotApi(options.client, sender)
64
68
 
69
+ this.api = api
65
70
  this.tasks = tasks
66
71
  this.router = new Router({
67
72
  sender,
68
73
  roles,
69
74
  gate,
70
75
  logger: options.logger,
76
+ ...(options.onEvent !== undefined
77
+ ? { onEvent: options.onEvent }
78
+ : {}),
71
79
  })
72
80
 
73
81
  let host: PluginHost
@@ -131,6 +139,22 @@ export class Kernel {
131
139
  return this.lifecycle.state
132
140
  }
133
141
 
142
+ get uptimeMs(): number {
143
+ return this.startedAt === null ? 0 : Date.now() - this.startedAt
144
+ }
145
+
146
+ get isConnected(): boolean {
147
+ return this.client.isConnected
148
+ }
149
+
150
+ listPlugins() {
151
+ return this.host.listStatus()
152
+ }
153
+
154
+ listCommands() {
155
+ return this.router.listCommands()
156
+ }
157
+
134
158
  start(): Promise<void> {
135
159
  return this.lifecycle.start()
136
160
  }
@@ -0,0 +1,109 @@
1
+ import {
2
+ formatValue,
3
+ logLevelWeight,
4
+ type LogLevel,
5
+ type LogRecord,
6
+ } from './logger.js'
7
+
8
+ export interface LogEntry {
9
+ time: number
10
+ level: LogLevel
11
+ scope: string
12
+ message: string
13
+ args: string[]
14
+ }
15
+
16
+ export interface LogFilter {
17
+ level?: LogLevel | undefined
18
+ scope?: string | undefined
19
+ query?: string | undefined
20
+ limit?: number | undefined
21
+ }
22
+
23
+ export function matchesLogFilter(entry: LogEntry, filter: LogFilter): boolean {
24
+ if (
25
+ filter.level !== undefined &&
26
+ logLevelWeight(entry.level) < logLevelWeight(filter.level)
27
+ ) {
28
+ return false
29
+ }
30
+
31
+ if (
32
+ filter.scope !== undefined &&
33
+ filter.scope.length > 0 &&
34
+ entry.scope !== filter.scope
35
+ ) {
36
+ return false
37
+ }
38
+
39
+ if (filter.query !== undefined && filter.query.length > 0) {
40
+ const query = filter.query.toLowerCase()
41
+ const haystack = [entry.scope, entry.message, ...entry.args]
42
+ .join(' ')
43
+ .toLowerCase()
44
+
45
+ if (!haystack.includes(query)) return false
46
+ }
47
+
48
+ return true
49
+ }
50
+
51
+ export class LogBuffer {
52
+ private readonly entries: LogEntry[] = []
53
+ private readonly listeners = new Set<(entry: LogEntry) => void>()
54
+
55
+ constructor(private readonly capacity = 2_000) {}
56
+
57
+ get size(): number {
58
+ return this.entries.length
59
+ }
60
+
61
+ push(record: LogRecord): void {
62
+ const entry: LogEntry = {
63
+ time: record.time.getTime(),
64
+ level: record.level,
65
+ scope: record.scope,
66
+ message: record.message,
67
+ args: record.args.map(formatValue),
68
+ }
69
+
70
+ this.entries.push(entry)
71
+
72
+ if (this.entries.length > this.capacity) {
73
+ this.entries.splice(0, this.entries.length - this.capacity)
74
+ }
75
+
76
+ for (const listener of this.listeners) {
77
+ try {
78
+ listener(entry)
79
+ } catch {
80
+ // Log consumers must never break logging.
81
+ }
82
+ }
83
+ }
84
+
85
+ list(filter: LogFilter = {}): LogEntry[] {
86
+ const matched = this.entries.filter((entry) =>
87
+ matchesLogFilter(entry, filter)
88
+ )
89
+ const limit = filter.limit
90
+
91
+ if (limit === undefined || limit < 0 || limit >= matched.length) {
92
+ return matched
93
+ }
94
+
95
+ return matched.slice(-limit)
96
+ }
97
+
98
+ clear(): void {
99
+ this.entries.length = 0
100
+ }
101
+
102
+ subscribe(listener: (entry: LogEntry) => void): () => void {
103
+ this.listeners.add(listener)
104
+
105
+ return () => {
106
+ this.listeners.delete(listener)
107
+ }
108
+ }
109
+ }
@@ -34,7 +34,7 @@ export interface Logger {
34
34
  child(scope: string): Logger
35
35
  }
36
36
 
37
- function formatValue(value: unknown): string {
37
+ export function formatValue(value: unknown): string {
38
38
  if (value instanceof Error) {
39
39
  return value.stack ?? `${value.name}: ${value.message}`
40
40
  }
@@ -49,7 +49,7 @@ function formatValue(value: unknown): string {
49
49
  }
50
50
  }
51
51
 
52
- const defaultSink: LogSink = (record) => {
52
+ export const defaultLogSink: LogSink = (record) => {
53
53
  const time = record.time.toISOString()
54
54
  const scope = record.scope ? ` [${record.scope}]` : ''
55
55
  const args =
@@ -92,10 +92,14 @@ export function parseLogLevel(
92
92
  return normalized
93
93
  }
94
94
 
95
+ export function logLevelWeight(level: LogLevel): number {
96
+ return LEVEL_WEIGHT[level]
97
+ }
98
+
95
99
  export function createLogger(options: LoggerOptions = {}): Logger {
96
100
  const threshold = options.level ?? 'info'
97
101
  const scope = options.scope ?? 'bot'
98
- const sink = options.sink ?? defaultSink
102
+ const sink = options.sink ?? defaultLogSink
99
103
 
100
104
  const emit = (level: LogLevel, message: string, args: unknown[]): void => {
101
105
  if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[threshold]) return
@@ -26,6 +26,21 @@ export interface SessionStore {
26
26
  clear(sessionId: string): Promise<void>
27
27
  }
28
28
 
29
+ export interface SessionSummary {
30
+ sessionId: string
31
+ provider?: string | undefined
32
+ model?: string | undefined
33
+ persona?: string | undefined
34
+ messageCount: number
35
+ updatedAt: number
36
+ }
37
+
38
+ export interface SessionListOptions {
39
+ limit?: number | undefined
40
+ offset?: number | undefined
41
+ query?: string | undefined
42
+ }
43
+
29
44
  interface SessionRow {
30
45
  session_id: string
31
46
  provider: string | null
@@ -35,6 +50,15 @@ interface SessionRow {
35
50
  updated_at: number
36
51
  }
37
52
 
53
+ interface SessionSummaryRow {
54
+ session_id: string
55
+ provider: string | null
56
+ model: string | null
57
+ persona: string | null
58
+ message_count: number
59
+ updated_at: number
60
+ }
61
+
38
62
  function isSessionRole(value: unknown): value is SessionRole {
39
63
  return value === 'system' || value === 'user' || value === 'assistant'
40
64
  }
@@ -79,6 +103,29 @@ function toState(row: SessionRow): SessionState {
79
103
  }
80
104
  }
81
105
 
106
+ function toSummary(row: SessionSummaryRow): SessionSummary {
107
+ return {
108
+ sessionId: row.session_id,
109
+ ...(row.provider !== null ? { provider: row.provider } : {}),
110
+ ...(row.model !== null ? { model: row.model } : {}),
111
+ ...(row.persona !== null ? { persona: row.persona } : {}),
112
+ messageCount: row.message_count,
113
+ updatedAt: row.updated_at,
114
+ }
115
+ }
116
+
117
+ function clampLimit(value: number | undefined, fallback: number): number {
118
+ if (value === undefined || !Number.isFinite(value)) return fallback
119
+
120
+ return Math.min(Math.max(Math.trunc(value), 1), 500)
121
+ }
122
+
123
+ function clampOffset(value: number | undefined): number {
124
+ if (value === undefined || !Number.isFinite(value)) return 0
125
+
126
+ return Math.max(Math.trunc(value), 0)
127
+ }
128
+
82
129
  export class SqliteSessionStore implements SessionStore {
83
130
  constructor(private readonly database: Database) {}
84
131
 
@@ -167,4 +214,52 @@ export class SqliteSessionStore implements SessionStore {
167
214
  .prepare('DELETE FROM sessions WHERE session_id = ?')
168
215
  .run(sessionId)
169
216
  }
217
+
218
+ async list(options: SessionListOptions = {}): Promise<SessionSummary[]> {
219
+ const limit = clampLimit(options.limit, 50)
220
+ const offset = clampOffset(options.offset)
221
+ const query = options.query?.trim()
222
+
223
+ const rows = (query
224
+ ? this.database.connection
225
+ .prepare(
226
+ `SELECT session_id, provider, model, persona,
227
+ json_array_length(messages) AS message_count,
228
+ updated_at
229
+ FROM sessions
230
+ WHERE session_id LIKE ?
231
+ ORDER BY updated_at DESC
232
+ LIMIT ? OFFSET ?`
233
+ )
234
+ .all(`%${query}%`, limit, offset)
235
+ : this.database.connection
236
+ .prepare(
237
+ `SELECT session_id, provider, model, persona,
238
+ json_array_length(messages) AS message_count,
239
+ updated_at
240
+ FROM sessions
241
+ ORDER BY updated_at DESC
242
+ LIMIT ? OFFSET ?`
243
+ )
244
+ .all(limit, offset)) as unknown as SessionSummaryRow[]
245
+
246
+ return rows.map(toSummary)
247
+ }
248
+
249
+ async count(query?: string): Promise<number> {
250
+ const value = query?.trim()
251
+ const row = (
252
+ value
253
+ ? this.database.connection
254
+ .prepare(
255
+ 'SELECT COUNT(*) AS count FROM sessions WHERE session_id LIKE ?'
256
+ )
257
+ .get(`%${value}%`)
258
+ : this.database.connection
259
+ .prepare('SELECT COUNT(*) AS count FROM sessions')
260
+ .get()
261
+ ) as { count: number } | undefined
262
+
263
+ return row?.count ?? 0
264
+ }
170
265
  }