@geminixiang/mikan 0.5.0 → 0.5.2
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 +13 -0
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/slack/blocks.d.ts +6 -0
- package/dist/adapters/slack/blocks.d.ts.map +1 -0
- package/dist/adapters/slack/blocks.js +176 -0
- package/dist/adapters/slack/blocks.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +0 -1
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +12 -34
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +0 -25
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +40 -46
- package/dist/agent.js.map +1 -1
- package/dist/tools/index.d.ts +1 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/admin/portal.js +125 -2
- package/dist/web/admin/portal.js.map +1 -1
- package/package.json +1 -1
- package/dist/adapters/slack/tools/block-kit.d.ts +0 -16
- package/dist/adapters/slack/tools/block-kit.d.ts.map +0 -1
- package/dist/adapters/slack/tools/block-kit.js +0 -105
- package/dist/adapters/slack/tools/block-kit.js.map +0 -1
- package/dist/tool-diagnostics.d.ts +0 -2
- package/dist/tool-diagnostics.d.ts.map +0 -1
- package/dist/tool-diagnostics.js +0 -7
- package/dist/tool-diagnostics.js.map +0 -1
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { execFile } from \"child_process\";\nimport { promisify } from \"util\";\n\nconst execFileAsync = promisify(execFile);\ntype ExecFileAsync = typeof execFileAsync;\n\n// ── adapter ───────────────────────────────────────────────────────────────────\n\nexport type ConversationKind = \"direct\" | \"shared\";\n\nexport type PlatformName = \"slack\" | \"discord\" | \"telegram\";\n\nexport interface ConversationMessage {\n id: string;\n sessionKey: string;\n conversationKind: ConversationKind;\n userId: string;\n userName?: string;\n text: string;\n attachments?: { name: string; localPath: string }[];\n threadTs?: string;\n}\n\nexport interface ChatToolResult {\n toolName: string;\n label?: string;\n args?: Record<string, unknown>;\n result: string;\n isError: boolean;\n durationMs: number;\n}\n\nexport interface ChatResponseBlockKit {\n text: string;\n blocks: object[];\n}\n\nexport interface ConversationResponder {\n respond(text: string): Promise<void>;\n appendResponseDelta?(delta: string): Promise<void>;\n finishResponse?(finalText?: string): Promise<void>;\n replaceResponse(text: string, options?: { createOverflowLink?: () => string }): Promise<void>;\n respondDiagnostic(text: string, options?: { style?: \"muted\" | \"error\" }): Promise<void>;\n respondToolResult(result: ChatToolResult): Promise<void>;\n respondBlockKit?(response: ChatResponseBlockKit): Promise<void>;\n setTyping(isTyping: boolean): Promise<void>;\n setWorking(working: boolean): Promise<void>;\n uploadFile(filePath: string, title?: string): Promise<void>;\n deleteResponse(): Promise<void>;\n}\n\nexport interface MessagingInfo {\n name: string;\n formattingGuide: string;\n channels: { id: string; name: string }[];\n users: { id: string; userName: string; displayName: string }[];\n diagnostics?: {\n showUsageSummary?: boolean;\n };\n}\n\nexport interface ChatAdapter {\n start(): Promise<void>;\n stop(): Promise<void>;\n getMessagingInfo(): MessagingInfo;\n}\n\nexport type AgentEventPayload =\n | { kind: \"sessionStart\" }\n | { kind: \"toolStart\"; toolId: string; toolName: string; input?: unknown }\n | { kind: \"toolEnd\"; toolId: string }\n | { kind: \"turnEnd\"; awaitingInput?: boolean }\n | { kind: \"sessionEnd\"; reason?: string };\n\nexport interface AgentEventEnvelope {\n source: \"mikan\";\n sessionId: string;\n actorName: string;\n event: AgentEventPayload;\n}\n\n/**\n * A platform-agnostic event (message/mention) that triggers the agent.\n */\nexport interface ConversationEvent {\n type: string;\n /** Platform-specific raw conversation/channel/chat identifier */\n conversationId: string;\n /** Optional alternate conversation identity used for vault routing. */\n vaultConversationId?: string;\n /** Cross-platform conversation shape: direct message vs shared space */\n conversationKind: ConversationKind;\n /** Message timestamp or ID as string */\n ts: string;\n /** Parent message ID for threaded replies (optional) */\n thread_ts?: string;\n /** User ID */\n user: string;\n /** Message text (already stripped of bot mentions) */\n text: string;\n /** Downloaded attachments */\n attachments?: { name: string; localPath: string }[];\n /** Platform-computed session key; overrides default conversationId:thread_ts computation */\n sessionKey?: string;\n}\n\n/**\n * Minimum interface that every platform bot must implement,\n * used by the central handler in main.ts and by EventsWatcher.\n */\nexport interface MessagingBot {\n start(): Promise<void>;\n postMessage(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: ConversationEvent): boolean;\n getMessagingInfo(): MessagingInfo;\n postPrivate?(conversationId: string, userId: string, text: string): Promise<void>;\n postPrivateDiagnostic?(\n conversationId: string,\n userId: string,\n text: string,\n options?: { style?: \"muted\" | \"error\" },\n ): Promise<void>;\n}\n\n/** Normalized platform data and reply hook for one event. */\nexport interface ConversationContext {\n message: ConversationMessage;\n responder: ConversationResponder;\n platform: MessagingInfo;\n}\n\nexport interface RunningSession {\n sessionKey: string;\n startedAt: number;\n lastActivityAt?: number;\n currentTool?: string;\n}\n\nexport interface MessagingEventHandler {\n isRunning(sessionKey: string): boolean;\n getRunningSessions(): RunningSession[];\n handleEvent(\n event: ConversationEvent,\n bot: MessagingBot,\n context: ConversationContext,\n ): Promise<void>;\n handleStop(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;\n forceStop(sessionKey: string): void;\n handleNewCommand(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;\n}\n\n// ── agent ─────────────────────────────────────────────────────────────────────\n\nexport interface PiAgentWrapper {\n syncChatHistory(currentMessageId?: string): void;\n run(\n message: ConversationMessage,\n responder: ConversationResponder,\n platform: MessagingInfo,\n ): Promise<{ stopReason: string; errorMessage?: string }>;\n abort(): void;\n getCurrentStep(): { toolName?: string; label?: string } | undefined;\n}\n\n// ── config ────────────────────────────────────────────────────────────────────\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n sandboxBoostCpus?: string;\n sandboxBoostMemory?: string;\n sandboxImageWorkspaceMount?: \"private\" | \"full\";\n defaultSharedVault?: string;\n slack?: {\n replyMode?: \"top-level\" | \"thread\";\n };\n}\n\nexport interface AutoReplyConfig {\n enabled: boolean;\n rules: string[];\n}\n\nexport interface JudgeModelConfig {\n provider: string;\n model: string;\n}\n\n// ── context ───────────────────────────────────────────────────────────────────\n\n/**\n * Platform conversation history entry from log.jsonl.\n */\nexport interface ConversationLogMessage {\n date?: string;\n ts?: string;\n threadTs?: string;\n user?: string;\n userName?: string;\n text?: string;\n isMessagingBot?: boolean;\n}\n\n// ── events ────────────────────────────────────────────────────────────────────\n\nexport interface ImmediateEvent {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n}\n\nexport interface OneShotEvent {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n at: string;\n}\n\nexport interface PeriodicEvent {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n schedule: string;\n timezone: string;\n}\n\nexport type MikanEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\nexport interface PeriodicEventInfo {\n filename: string;\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n text: string;\n schedule: string;\n timezone: string;\n nextRun: string | null;\n}\n\n// ── execution-resolver ────────────────────────────────────────────────────────\n\nexport interface ActorContext {\n platform: string;\n userId: string;\n conversationId: string;\n}\n\nexport type ImageWorkspaceMountMode = \"private\" | \"full\";\n\n// ── log ───────────────────────────────────────────────────────────────────────\n\nexport interface LogContext {\n conversationId: string;\n userName?: string;\n conversationName?: string;\n sessionId?: string;\n}\n\n// ── portal-shell ──────────────────────────────────────────────────────────────\n\ntype PortalView = \"admin\" | \"session\" | \"vault\";\n\nexport interface PortalShellOptions {\n activeView: PortalView;\n pageTitle: string;\n identity?: {\n primary: string;\n secondary?: string;\n };\n conversationSwitcher?: {\n currentId: string;\n options?: Array<{ id: string; label: string; running?: boolean }>;\n };\n navLinks?: Partial<Record<PortalView, string>>;\n body: string;\n extraStyles?: string;\n inlineScript?: string;\n extraHead?: string;\n bodyAttributes?: Record<string, string>;\n}\n\n// ── provisioner ───────────────────────────────────────────────────────────────\n\nexport interface ContainerMount {\n source: string;\n target: string;\n}\n\nexport interface ResourceLimits {\n cpus?: string;\n memory?: string;\n}\n\nexport interface SandboxLimitStatus {\n limits?: ResourceLimits;\n boosted: boolean;\n}\n\nexport interface ProvisionOptions {\n containerName?: string;\n mounts?: ContainerMount[];\n conversationId?: string;\n}\n\nexport interface DockerContainerManagerOptions {\n limits?: ResourceLimits;\n boostLimits?: ResourceLimits;\n execFileImpl?: ExecFileAsync;\n}\n\n// ── store ─────────────────────────────────────────────────────────────────────\n\nexport interface Attachment {\n original: string;\n localPath: string;\n}\n\nexport interface LoggedMessage {\n date: string;\n ts: string;\n user: string;\n userName?: string;\n displayName?: string;\n text: string;\n attachments: Attachment[];\n isMessagingBot: boolean;\n threadTs?: string;\n}\n\nexport interface ChannelStoreConfig {\n workingDir: string;\n botToken: string;\n}\n\n// ── trigger ───────────────────────────────────────────────────────────────────\n\nexport type TriggerIntent = \"mention\" | \"direct\" | \"thread-continuation\" | \"auto-reply-candidate\";\n\nexport type TriggerResult = { trigger: true; reason: string } | { trigger: false; reason: string };\n\nexport type AutoReplyJudge = (input: {\n event: ConversationEvent;\n rules: string[];\n conversationDir: string;\n}) => Promise<boolean>;\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { execFile } from \"child_process\";\nimport { promisify } from \"util\";\n\nconst execFileAsync = promisify(execFile);\ntype ExecFileAsync = typeof execFileAsync;\n\n// ── adapter ───────────────────────────────────────────────────────────────────\n\nexport type ConversationKind = \"direct\" | \"shared\";\n\nexport type PlatformName = \"slack\" | \"discord\" | \"telegram\";\n\nexport interface ConversationMessage {\n id: string;\n sessionKey: string;\n conversationKind: ConversationKind;\n userId: string;\n userName?: string;\n text: string;\n attachments?: { name: string; localPath: string }[];\n threadTs?: string;\n}\n\nexport interface ChatToolResult {\n toolName: string;\n label?: string;\n args?: Record<string, unknown>;\n result: string;\n isError: boolean;\n durationMs: number;\n}\n\nexport interface ConversationResponder {\n respond(text: string): Promise<void>;\n appendResponseDelta?(delta: string): Promise<void>;\n finishResponse?(finalText?: string): Promise<void>;\n replaceResponse(text: string, options?: { createOverflowLink?: () => string }): Promise<void>;\n respondDiagnostic(text: string, options?: { style?: \"muted\" | \"error\" }): Promise<void>;\n respondToolResult(result: ChatToolResult): Promise<void>;\n setTyping(isTyping: boolean): Promise<void>;\n setWorking(working: boolean): Promise<void>;\n uploadFile(filePath: string, title?: string): Promise<void>;\n deleteResponse(): Promise<void>;\n}\n\nexport interface MessagingInfo {\n name: string;\n formattingGuide: string;\n channels: { id: string; name: string }[];\n users: { id: string; userName: string; displayName: string }[];\n diagnostics?: {\n showUsageSummary?: boolean;\n };\n}\n\nexport interface ChatAdapter {\n start(): Promise<void>;\n stop(): Promise<void>;\n getMessagingInfo(): MessagingInfo;\n}\n\nexport type AgentEventPayload =\n | { kind: \"sessionStart\" }\n | { kind: \"toolStart\"; toolId: string; toolName: string; input?: unknown }\n | { kind: \"toolEnd\"; toolId: string }\n | { kind: \"turnEnd\"; awaitingInput?: boolean }\n | { kind: \"sessionEnd\"; reason?: string };\n\nexport interface AgentEventEnvelope {\n source: \"mikan\";\n sessionId: string;\n actorName: string;\n event: AgentEventPayload;\n}\n\n/**\n * A platform-agnostic event (message/mention) that triggers the agent.\n */\nexport interface ConversationEvent {\n type: string;\n /** Platform-specific raw conversation/channel/chat identifier */\n conversationId: string;\n /** Optional alternate conversation identity used for vault routing. */\n vaultConversationId?: string;\n /** Cross-platform conversation shape: direct message vs shared space */\n conversationKind: ConversationKind;\n /** Message timestamp or ID as string */\n ts: string;\n /** Parent message ID for threaded replies (optional) */\n thread_ts?: string;\n /** User ID */\n user: string;\n /** Message text (already stripped of bot mentions) */\n text: string;\n /** Downloaded attachments */\n attachments?: { name: string; localPath: string }[];\n /** Platform-computed session key; overrides default conversationId:thread_ts computation */\n sessionKey?: string;\n}\n\n/**\n * Minimum interface that every platform bot must implement,\n * used by the central handler in main.ts and by EventsWatcher.\n */\nexport interface MessagingBot {\n start(): Promise<void>;\n postMessage(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: ConversationEvent): boolean;\n getMessagingInfo(): MessagingInfo;\n postPrivate?(conversationId: string, userId: string, text: string): Promise<void>;\n postPrivateDiagnostic?(\n conversationId: string,\n userId: string,\n text: string,\n options?: { style?: \"muted\" | \"error\" },\n ): Promise<void>;\n}\n\n/** Normalized platform data and reply hook for one event. */\nexport interface ConversationContext {\n message: ConversationMessage;\n responder: ConversationResponder;\n platform: MessagingInfo;\n}\n\nexport interface RunningSession {\n sessionKey: string;\n startedAt: number;\n lastActivityAt?: number;\n currentTool?: string;\n}\n\nexport interface MessagingEventHandler {\n isRunning(sessionKey: string): boolean;\n getRunningSessions(): RunningSession[];\n handleEvent(\n event: ConversationEvent,\n bot: MessagingBot,\n context: ConversationContext,\n ): Promise<void>;\n handleStop(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;\n forceStop(sessionKey: string): void;\n handleNewCommand(sessionKey: string, conversationId: string, bot: MessagingBot): Promise<void>;\n}\n\n// ── agent ─────────────────────────────────────────────────────────────────────\n\nexport interface PiAgentWrapper {\n syncChatHistory(currentMessageId?: string): void;\n run(\n message: ConversationMessage,\n responder: ConversationResponder,\n platform: MessagingInfo,\n ): Promise<{ stopReason: string; errorMessage?: string }>;\n abort(): void;\n getCurrentStep(): { toolName?: string; label?: string } | undefined;\n}\n\n// ── config ────────────────────────────────────────────────────────────────────\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n sandboxBoostCpus?: string;\n sandboxBoostMemory?: string;\n sandboxImageWorkspaceMount?: \"private\" | \"full\";\n defaultSharedVault?: string;\n slack?: {\n replyMode?: \"top-level\" | \"thread\";\n };\n}\n\nexport interface AutoReplyConfig {\n enabled: boolean;\n rules: string[];\n}\n\nexport interface JudgeModelConfig {\n provider: string;\n model: string;\n}\n\n// ── context ───────────────────────────────────────────────────────────────────\n\n/**\n * Platform conversation history entry from log.jsonl.\n */\nexport interface ConversationLogMessage {\n date?: string;\n ts?: string;\n threadTs?: string;\n user?: string;\n userName?: string;\n text?: string;\n isMessagingBot?: boolean;\n}\n\n// ── events ────────────────────────────────────────────────────────────────────\n\nexport interface ImmediateEvent {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n}\n\nexport interface OneShotEvent {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n at: string;\n}\n\nexport interface PeriodicEvent {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n schedule: string;\n timezone: string;\n}\n\nexport type MikanEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\nexport interface PeriodicEventInfo {\n filename: string;\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n text: string;\n schedule: string;\n timezone: string;\n nextRun: string | null;\n}\n\n// ── execution-resolver ────────────────────────────────────────────────────────\n\nexport interface ActorContext {\n platform: string;\n userId: string;\n conversationId: string;\n}\n\nexport type ImageWorkspaceMountMode = \"private\" | \"full\";\n\n// ── log ───────────────────────────────────────────────────────────────────────\n\nexport interface LogContext {\n conversationId: string;\n userName?: string;\n conversationName?: string;\n sessionId?: string;\n}\n\n// ── portal-shell ──────────────────────────────────────────────────────────────\n\ntype PortalView = \"admin\" | \"session\" | \"vault\";\n\nexport interface PortalShellOptions {\n activeView: PortalView;\n pageTitle: string;\n identity?: {\n primary: string;\n secondary?: string;\n };\n conversationSwitcher?: {\n currentId: string;\n options?: Array<{ id: string; label: string; running?: boolean }>;\n };\n navLinks?: Partial<Record<PortalView, string>>;\n body: string;\n extraStyles?: string;\n inlineScript?: string;\n extraHead?: string;\n bodyAttributes?: Record<string, string>;\n}\n\n// ── provisioner ───────────────────────────────────────────────────────────────\n\nexport interface ContainerMount {\n source: string;\n target: string;\n}\n\nexport interface ResourceLimits {\n cpus?: string;\n memory?: string;\n}\n\nexport interface SandboxLimitStatus {\n limits?: ResourceLimits;\n boosted: boolean;\n}\n\nexport interface ProvisionOptions {\n containerName?: string;\n mounts?: ContainerMount[];\n conversationId?: string;\n}\n\nexport interface DockerContainerManagerOptions {\n limits?: ResourceLimits;\n boostLimits?: ResourceLimits;\n execFileImpl?: ExecFileAsync;\n}\n\n// ── store ─────────────────────────────────────────────────────────────────────\n\nexport interface Attachment {\n original: string;\n localPath: string;\n}\n\nexport interface LoggedMessage {\n date: string;\n ts: string;\n user: string;\n userName?: string;\n displayName?: string;\n text: string;\n attachments: Attachment[];\n isMessagingBot: boolean;\n threadTs?: string;\n}\n\nexport interface ChannelStoreConfig {\n workingDir: string;\n botToken: string;\n}\n\n// ── trigger ───────────────────────────────────────────────────────────────────\n\nexport type TriggerIntent = \"mention\" | \"direct\" | \"thread-continuation\" | \"auto-reply-candidate\";\n\nexport type TriggerResult = { trigger: true; reason: string } | { trigger: false; reason: string };\n\nexport type AutoReplyJudge = (input: {\n event: ConversationEvent;\n rules: string[];\n conversationDir: string;\n}) => Promise<boolean>;\n"]}
|
package/dist/web/admin/portal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
|
-
import { join, resolve as pathResolve, sep as pathSep } from "path";
|
|
4
|
-
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { basename, join, resolve as pathResolve, sep as pathSep } from "path";
|
|
4
|
+
import { AuthStorage, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { loadConversationAutoReplyConfig, loadGlobalSettings, resolveConversationSettings, saveConversationAutoReplyConfig, updateConversationSettings, updateGlobalSettings, } from "../../config.js";
|
|
6
6
|
import { escapeHtml } from "../../utils/html.js";
|
|
7
7
|
import { readRawBody } from "../../utils/http-body.js";
|
|
@@ -52,6 +52,10 @@ function routeApiRequest(req, res, url, services) {
|
|
|
52
52
|
serveConversationsList(res, services);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
+
if (url.pathname === "/admin/api/session-usage") {
|
|
56
|
+
serveSessionUsage(res, services);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
55
59
|
if (url.pathname === "/admin/api/conversation-state") {
|
|
56
60
|
serveConversationState(res, url, services, token);
|
|
57
61
|
return;
|
|
@@ -261,6 +265,70 @@ function serveConversationsList(res, services) {
|
|
|
261
265
|
});
|
|
262
266
|
jsonRes(res, 200, { conversations });
|
|
263
267
|
}
|
|
268
|
+
function serveSessionUsage(res, services) {
|
|
269
|
+
const workingDir = requireAdminWorkingDir(res, services);
|
|
270
|
+
if (!workingDir)
|
|
271
|
+
return;
|
|
272
|
+
const rows = listConversationDirs(workingDir)
|
|
273
|
+
.flatMap((conversationId) => listConversationSessionUsage(workingDir, conversationId, conversationDisplayLabel(services, conversationId)))
|
|
274
|
+
.toSorted((a, b) => b.total - a.total)
|
|
275
|
+
.slice(0, 20);
|
|
276
|
+
jsonRes(res, 200, { sessions: rows });
|
|
277
|
+
}
|
|
278
|
+
function listConversationSessionUsage(workingDir, conversationId, label) {
|
|
279
|
+
const sessionDir = join(workingDir, conversationId, "sessions");
|
|
280
|
+
try {
|
|
281
|
+
return readdirSync(sessionDir, { withFileTypes: true })
|
|
282
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
283
|
+
.flatMap((entry) => readSessionUsage(join(sessionDir, entry.name), conversationId, label));
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function readSessionUsage(sessionFile, conversationId, label) {
|
|
290
|
+
try {
|
|
291
|
+
const manager = SessionManager.open(sessionFile);
|
|
292
|
+
const header = manager.getHeader();
|
|
293
|
+
if (!header)
|
|
294
|
+
return [];
|
|
295
|
+
const entries = manager.getEntries();
|
|
296
|
+
const usage = entries.reduce((sum, entry) => {
|
|
297
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
298
|
+
return sum;
|
|
299
|
+
const message = entry.message;
|
|
300
|
+
const item = message.usage;
|
|
301
|
+
if (!item)
|
|
302
|
+
return sum;
|
|
303
|
+
sum.input += numberOrZero(item.input);
|
|
304
|
+
sum.output += numberOrZero(item.output);
|
|
305
|
+
sum.cacheRead += numberOrZero(item.cacheRead);
|
|
306
|
+
sum.cacheWrite += numberOrZero(item.cacheWrite);
|
|
307
|
+
sum.cost += numberOrZero(item.cost?.total);
|
|
308
|
+
return sum;
|
|
309
|
+
}, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
310
|
+
const total = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
311
|
+
if (total <= 0)
|
|
312
|
+
return [];
|
|
313
|
+
return [
|
|
314
|
+
{
|
|
315
|
+
conversationId,
|
|
316
|
+
label,
|
|
317
|
+
fileName: basename(sessionFile),
|
|
318
|
+
sessionId: header.id,
|
|
319
|
+
updatedAt: entries.at(-1)?.timestamp ?? header.timestamp,
|
|
320
|
+
...usage,
|
|
321
|
+
total,
|
|
322
|
+
},
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function numberOrZero(value) {
|
|
330
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
331
|
+
}
|
|
264
332
|
function serveConversationState(res, url, services, token) {
|
|
265
333
|
const workingDir = requireAdminWorkingDir(res, services);
|
|
266
334
|
if (!workingDir)
|
|
@@ -1196,6 +1264,17 @@ function renderAdminPage(token) {
|
|
|
1196
1264
|
<div id="all-conv-content"><div class="loading-msg">Loading…</div></div>
|
|
1197
1265
|
</section>
|
|
1198
1266
|
|
|
1267
|
+
<section class="card sect">
|
|
1268
|
+
<header class="sect-head">
|
|
1269
|
+
<div>
|
|
1270
|
+
<p class="eyebrow">Token Usage</p>
|
|
1271
|
+
<h2 class="card-title">Top 20 sessions</h2>
|
|
1272
|
+
</div>
|
|
1273
|
+
<button class="refresh-btn" onclick="loadSessionUsage()">↻</button>
|
|
1274
|
+
</header>
|
|
1275
|
+
<div id="session-usage-content"><div class="loading-msg">Loading…</div></div>
|
|
1276
|
+
</section>
|
|
1277
|
+
|
|
1199
1278
|
<section class="card sect">
|
|
1200
1279
|
<header class="sect-head">
|
|
1201
1280
|
<div>
|
|
@@ -1724,6 +1803,7 @@ function renderAdminPage(token) {
|
|
|
1724
1803
|
if (globalLoaded) return;
|
|
1725
1804
|
globalLoaded = true;
|
|
1726
1805
|
loadAllConversations();
|
|
1806
|
+
loadSessionUsage();
|
|
1727
1807
|
loadGlobalSettings();
|
|
1728
1808
|
loadGlobalSkills();
|
|
1729
1809
|
loadEvents();
|
|
@@ -1751,6 +1831,37 @@ function renderAdminPage(token) {
|
|
|
1751
1831
|
}
|
|
1752
1832
|
}
|
|
1753
1833
|
|
|
1834
|
+
async function loadSessionUsage() {
|
|
1835
|
+
const container = document.getElementById('session-usage-content');
|
|
1836
|
+
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
1837
|
+
try {
|
|
1838
|
+
const data = await apiGet('/admin/api/session-usage');
|
|
1839
|
+
if (data.sessions.length === 0) {
|
|
1840
|
+
container.innerHTML = '<div class="empty-state">No token usage found</div>';
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
container.innerHTML = '<div class="usage-table-wrap"><table class="usage-table"><thead><tr><th>#</th><th>Channel</th><th>Session</th><th>Updated</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Total</th><th>Cost</th></tr></thead><tbody>' +
|
|
1844
|
+
data.sessions.map((s, i) => '<tr>' +
|
|
1845
|
+
'<td>' + (i + 1) + '</td>' +
|
|
1846
|
+
'<td>' + escHtml(s.label || s.conversationId) + '</td>' +
|
|
1847
|
+
'<td><code>' + escHtml(s.fileName) + '</code></td>' +
|
|
1848
|
+
'<td>' + escHtml(new Date(s.updatedAt).toLocaleString()) + '</td>' +
|
|
1849
|
+
'<td>' + fmtNum(s.input) + '</td>' +
|
|
1850
|
+
'<td>' + fmtNum(s.output) + '</td>' +
|
|
1851
|
+
'<td>' + fmtNum(s.cacheRead) + '</td>' +
|
|
1852
|
+
'<td>' + fmtNum(s.cacheWrite) + '</td>' +
|
|
1853
|
+
'<td><strong>' + fmtNum(s.total) + '</strong></td>' +
|
|
1854
|
+
'<td>' + (s.cost > 0 ? '$' + Number(s.cost).toFixed(4) : '—') + '</td>' +
|
|
1855
|
+
'</tr>').join('') + '</tbody></table></div>';
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function fmtNum(value) {
|
|
1862
|
+
return Number(value || 0).toLocaleString('en-US');
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1754
1865
|
async function loadGlobalSettings() {
|
|
1755
1866
|
const container = document.getElementById('global-settings-content');
|
|
1756
1867
|
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
@@ -2110,6 +2221,18 @@ const adminViewStyles = `
|
|
|
2110
2221
|
.conv-id { flex: 1; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.84rem; }
|
|
2111
2222
|
.conv-last { color: var(--subtle); font-size: 0.78rem; }
|
|
2112
2223
|
|
|
2224
|
+
.usage-table-wrap { overflow-x: auto; }
|
|
2225
|
+
.usage-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
|
|
2226
|
+
.usage-table th, .usage-table td {
|
|
2227
|
+
padding: 8px 10px; border-bottom: 1px solid var(--border);
|
|
2228
|
+
text-align: left; white-space: nowrap;
|
|
2229
|
+
}
|
|
2230
|
+
.usage-table th {
|
|
2231
|
+
color: var(--subtle); font-size: 0.68rem;
|
|
2232
|
+
text-transform: uppercase; letter-spacing: 0.08em;
|
|
2233
|
+
}
|
|
2234
|
+
.usage-table code { font-size: 0.72rem; }
|
|
2235
|
+
|
|
2113
2236
|
.status-pill {
|
|
2114
2237
|
display: inline-flex; padding: 2px 9px; border-radius: 999px;
|
|
2115
2238
|
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
|