Skip to content

Commit c50f9fe

Browse files
authored
Merge pull request #102 from TraderAlice/dev
feat: session awareness tools, dev workbench, telegram onboarding
2 parents 97470b8 + 89fec76 commit c50f9fe

16 files changed

Lines changed: 934 additions & 22 deletions

File tree

src/connectors/mcp-ask/mcp-ask-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class McpAskPlugin implements Plugin {
6161
const session = await plugin.getSession(sessionId)
6262

6363
const result = await ctx.agentCenter.askWithSession(message, session, {
64-
historyPreamble: 'The following is the conversation from an external MCP client. Use it as context if the caller references earlier messages.',
64+
historyPreamble: `You are operating via the MCP Ask connector (session: mcp-ask__${sessionId}). The following is the conversation from an external MCP client.`,
6565
})
6666

6767
return {

src/connectors/telegram/telegram-plugin.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { askAgentSdk } from '../../ai-providers/agent-sdk/query.js'
1010
import type { AgentSdkConfig } from '../../ai-providers/agent-sdk/query.js'
1111
import { SessionStore } from '../../core/session'
1212
import { forceCompact } from '../../core/compaction'
13-
import { readAIBackend, writeAIBackend, type AIBackend } from '../../core/config'
13+
import { readAIBackend, writeAIBackend, readConnectorsConfig, type AIBackend } from '../../core/config'
1414
import type { ConnectorCenter } from '../../core/connector-center.js'
1515
import { TelegramConnector, splitMessage, MAX_MESSAGE_LENGTH } from './telegram-connector.js'
1616

@@ -34,6 +34,7 @@ export class TelegramPlugin implements Plugin {
3434

3535
/** Throttle: last time we sent an auth-guidance reply per chatId. */
3636
private authReplyThrottle = new Map<number, number>()
37+
private webPort = 3002
3738

3839
constructor(
3940
config: Omit<TelegramConfig, 'pollingTimeout'> & { pollingTimeout?: number },
@@ -45,6 +46,7 @@ export class TelegramPlugin implements Plugin {
4546

4647
async start(engineCtx: EngineContext) {
4748
this.connectorCenter = engineCtx.connectorCenter
49+
this.webPort = engineCtx.config.connectors.web.port
4850

4951
// Inject agent config into Claude Code config (used by /compact command)
5052
this.agentSdkConfig = {
@@ -63,19 +65,21 @@ export class TelegramPlugin implements Plugin {
6365
console.error('telegram bot error:', err)
6466
})
6567

66-
// ── Middleware: auth guard (always active) ──
68+
// ── Middleware: auth guard (hot-reloads chatIds from connectors.json) ──
6769
bot.use(async (ctx, next) => {
6870
const chatId = ctx.chat?.id
6971
if (!chatId) return
70-
if (this.config.allowedChatIds.includes(chatId)) return next()
72+
const { telegram } = await readConnectorsConfig()
73+
if (telegram.chatIds.includes(chatId)) return next()
7174

7275
// Unauthorized — log chat ID for operator, throttle reply (60s)
7376
const now = Date.now()
7477
const last = this.authReplyThrottle.get(chatId) ?? 0
7578
if (now - last > 60_000) {
7679
this.authReplyThrottle.set(chatId, now)
77-
console.log(`telegram: unauthorized chat ${chatId}, set TELEGRAM_CHAT_ID=${chatId} to allow`)
78-
await ctx.reply('This chat is not authorized. Add this chat ID to TELEGRAM_CHAT_ID in your environment config.').catch(() => {})
80+
const link = `http://localhost:${this.webPort}/connectors?addChatId=${chatId}`
81+
console.log(`telegram: unauthorized chat ${chatId}, authorize via ${link}`)
82+
await ctx.reply(`To authorize this chat, open:\n${link}`).catch(() => {})
7983
}
8084
})
8185

@@ -239,7 +243,7 @@ export class TelegramPlugin implements Plugin {
239243
// Route through AgentCenter → GenerateRouter → active provider
240244
const session = await this.getSession(message.from.id)
241245
const result = await engineCtx.agentCenter.askWithSession(prompt, session, {
242-
historyPreamble: 'The following is the recent conversation from this Telegram chat. Use it as context if the user references earlier messages.',
246+
historyPreamble: `You are operating via Telegram (session: telegram/${message.from.id}). The following is the recent conversation.`,
243247
})
244248
stopTyping()
245249
await this.sendReplyWithPlaceholder(message.chatId, result.text, result.media, placeholder?.message_id)

src/connectors/telegram/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type { Update, Message, Chat, User }
44

55
export interface TelegramConfig {
66
token: string
7-
/** Chat IDs allowed to interact. Empty = allow all. */
7+
/** Chat IDs allowed to interact. Empty = reject all. */
88
allowedChatIds: number[]
99
/** Polling timeout in seconds (Telegram long-poll parameter). Default: 30 */
1010
pollingTimeout: number

src/connectors/web/routes/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function createChatRoutes({ ctx, sessions, sseByChannel }: ChatDeps) {
3535

3636
// Build AskOptions from channel config (if not default)
3737
const opts: AskOptions = {
38-
historyPreamble: 'The following is the recent conversation from the Web UI. Use it as context if the user references earlier messages.',
38+
historyPreamble: `You are operating via the Web UI (session: web/${channelId}). The following is the recent conversation.`,
3939
}
4040
if (channelId !== 'default') {
4141
const channels = await readWebSubchannels()

src/connectors/web/routes/tools.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Hono } from 'hono'
2+
import { z } from 'zod'
23
import type { ToolCenter } from '../../../core/tool-center.js'
34
import { readToolsConfig, writeConfigSection } from '../../../core/config.js'
5+
import { extractMcpShape, wrapToolExecute } from '../../../core/mcp-export.js'
46

5-
/** Tools routes: GET / (inventory + disabled), PUT / (update disabled list) */
7+
/** Tools routes: inventory, detail, execute, enable/disable */
68
export function createToolsRoutes(toolCenter: ToolCenter) {
79
const app = new Hono()
810

11+
/** GET / — inventory + disabled list */
912
app.get('/', async (c) => {
1013
try {
1114
const inventory = toolCenter.getInventory()
@@ -16,6 +19,7 @@ export function createToolsRoutes(toolCenter: ToolCenter) {
1619
}
1720
})
1821

22+
/** PUT / — update disabled list */
1923
app.put('/', async (c) => {
2024
try {
2125
const body = await c.req.json()
@@ -29,5 +33,47 @@ export function createToolsRoutes(toolCenter: ToolCenter) {
2933
}
3034
})
3135

36+
/** GET /:name — full tool detail with JSON Schema */
37+
app.get('/:name', (c) => {
38+
const name = c.req.param('name')
39+
const tool = toolCenter.get(name)
40+
if (!tool) return c.json({ error: `Tool not found: ${name}` }, 404)
41+
42+
let inputSchema: unknown = {}
43+
try {
44+
inputSchema = z.toJSONSchema(tool.inputSchema as z.ZodType)
45+
} catch { /* fallback to empty */ }
46+
47+
return c.json({
48+
name,
49+
group: toolCenter.getGroup(name),
50+
description: tool.description ?? '',
51+
inputSchema,
52+
})
53+
})
54+
55+
/** POST /:name/execute — execute a tool with given input */
56+
app.post('/:name/execute', async (c) => {
57+
const name = c.req.param('name')
58+
const tool = toolCenter.get(name)
59+
if (!tool) return c.json({ error: `Tool not found: ${name}` }, 404)
60+
61+
const rawInput = await c.req.json().catch(() => ({}))
62+
63+
// Validate + coerce through MCP shape (handles string→number etc.)
64+
const shape = extractMcpShape(tool)
65+
const schema = z.object(shape)
66+
let validated: Record<string, unknown>
67+
try {
68+
validated = await schema.parseAsync(rawInput)
69+
} catch (err) {
70+
return c.json({ error: 'Validation failed', details: String(err) }, 400)
71+
}
72+
73+
const execute = wrapToolExecute(tool)
74+
const result = await execute(validated)
75+
return c.json(result)
76+
})
77+
3278
return app
3379
}

src/core/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,16 @@ export async function readToolsConfig() {
433433
}
434434
}
435435

436+
/** Read connectors config from disk (called per-request for hot-reload). */
437+
export async function readConnectorsConfig() {
438+
try {
439+
const raw = JSON.parse(await readFile(resolve(CONFIG_DIR, 'connectors.json'), 'utf-8'))
440+
return connectorsSchema.parse(raw)
441+
} catch {
442+
return connectorsSchema.parse({})
443+
}
444+
}
445+
436446
// ==================== AI Backend Helpers ====================
437447

438448
export type AIBackend = 'claude-code' | 'vercel-ai-sdk' | 'agent-sdk'

src/core/connector-center.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export class ConnectorCenter {
4747
private lastInteraction: LastInteraction | null = null
4848

4949
constructor(eventLog?: EventLog) {
50+
// Restore last interaction from event log buffer (survives restart)
51+
if (eventLog) {
52+
const recent = eventLog.recent({ type: 'message.received' })
53+
if (recent.length > 0) {
54+
const last = recent[recent.length - 1]
55+
const { channel, to } = last.payload as { channel: string; to: string }
56+
this.lastInteraction = { channel, to, ts: last.ts }
57+
}
58+
}
59+
60+
// Subscribe to future interactions
5061
eventLog?.subscribeType('message.received', (entry) => {
5162
const { channel, to } = entry.payload as { channel: string; to: string }
5263
this.touch(channel, to)

src/core/tool-center.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ export class ToolCenter {
5555
}))
5656
}
5757

58+
/** Look up a single tool by name (for detail / execute endpoints). */
59+
get(name: string): Tool | null {
60+
return this.tools[name]?.tool ?? null
61+
}
62+
63+
/** Look up a tool's group by name. */
64+
getGroup(name: string): string | null {
65+
return this.tools[name]?.group ?? null
66+
}
67+
5868
/** Tool name list (for logging / debugging). */
5969
list(): string[] {
6070
return Object.keys(this.tools)

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { OpenBBCommodityClient } from './domain/market-data/client/openbb-api/co
2626
import { OpenBBServerPlugin } from './server/opentypebb.js'
2727
import { createMarketSearchTools } from './tool/market.js'
2828
import { createAnalysisTools } from './tool/analysis.js'
29+
import { createSessionTools } from './tool/session.js'
2930
import { SessionStore } from './core/session.js'
3031
import { ConnectorCenter } from './core/connector-center.js'
3132
import { ToolCenter } from './core/tool-center.js'
@@ -227,6 +228,9 @@ async function main() {
227228

228229
const connectorCenter = new ConnectorCenter(eventLog)
229230

231+
// Session awareness tools (registered here because they need connectorCenter)
232+
toolCenter.register(createSessionTools(connectorCenter), 'session')
233+
230234
// ==================== Cron Lifecycle ====================
231235

232236
await cronEngine.start()

src/task/cron/listener.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function createCronListener(opts: CronListenerOpts): CronListener {
6363
try {
6464
// Ask the AI engine with the cron payload
6565
const result = await agentCenter.askWithSession(payload.payload, session, {
66-
historyPreamble: 'The following is the recent cron session conversation. This is an automated cron job execution.',
66+
historyPreamble: `You are operating in the cron job context (session: cron/default, job: ${payload.jobName}). This is an automated cron job execution.`,
6767
})
6868

6969
// Send notification through the last-interacted connector

0 commit comments

Comments
 (0)