talon-agent 1.0.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 (89) hide show
  1. package/README.md +137 -0
  2. package/bin/talon.js +5 -0
  3. package/package.json +86 -0
  4. package/prompts/base.md +13 -0
  5. package/prompts/custom.md.example +22 -0
  6. package/prompts/dream.md +41 -0
  7. package/prompts/identity.md +45 -0
  8. package/prompts/teams.md +52 -0
  9. package/prompts/telegram.md +89 -0
  10. package/prompts/terminal.md +13 -0
  11. package/src/__tests__/chat-id.test.ts +91 -0
  12. package/src/__tests__/chat-settings.test.ts +337 -0
  13. package/src/__tests__/config.test.ts +546 -0
  14. package/src/__tests__/cron-store.test.ts +440 -0
  15. package/src/__tests__/daily-log.test.ts +146 -0
  16. package/src/__tests__/dispatcher.test.ts +383 -0
  17. package/src/__tests__/errors.test.ts +240 -0
  18. package/src/__tests__/fuzz.test.ts +302 -0
  19. package/src/__tests__/gateway-actions.test.ts +1453 -0
  20. package/src/__tests__/gateway-context.test.ts +102 -0
  21. package/src/__tests__/gateway-http.test.ts +245 -0
  22. package/src/__tests__/handlers.test.ts +351 -0
  23. package/src/__tests__/history-persistence.test.ts +172 -0
  24. package/src/__tests__/history.test.ts +659 -0
  25. package/src/__tests__/integration.test.ts +189 -0
  26. package/src/__tests__/log.test.ts +110 -0
  27. package/src/__tests__/media-index.test.ts +277 -0
  28. package/src/__tests__/plugin.test.ts +317 -0
  29. package/src/__tests__/prompt-builder.test.ts +71 -0
  30. package/src/__tests__/sessions.test.ts +594 -0
  31. package/src/__tests__/teams-frontend.test.ts +239 -0
  32. package/src/__tests__/telegram.test.ts +177 -0
  33. package/src/__tests__/terminal-commands.test.ts +367 -0
  34. package/src/__tests__/terminal-frontend.test.ts +141 -0
  35. package/src/__tests__/terminal-renderer.test.ts +278 -0
  36. package/src/__tests__/watchdog.test.ts +287 -0
  37. package/src/__tests__/workspace.test.ts +184 -0
  38. package/src/backend/claude-sdk/index.ts +438 -0
  39. package/src/backend/claude-sdk/tools.ts +605 -0
  40. package/src/backend/opencode/index.ts +252 -0
  41. package/src/bootstrap.ts +134 -0
  42. package/src/cli.ts +611 -0
  43. package/src/core/cron.ts +148 -0
  44. package/src/core/dispatcher.ts +126 -0
  45. package/src/core/dream.ts +295 -0
  46. package/src/core/errors.ts +206 -0
  47. package/src/core/gateway-actions.ts +267 -0
  48. package/src/core/gateway.ts +258 -0
  49. package/src/core/plugin.ts +432 -0
  50. package/src/core/prompt-builder.ts +43 -0
  51. package/src/core/pulse.ts +175 -0
  52. package/src/core/types.ts +85 -0
  53. package/src/frontend/teams/actions.ts +101 -0
  54. package/src/frontend/teams/formatting.ts +220 -0
  55. package/src/frontend/teams/graph.ts +297 -0
  56. package/src/frontend/teams/index.ts +308 -0
  57. package/src/frontend/teams/proxy-fetch.ts +28 -0
  58. package/src/frontend/teams/tools.ts +177 -0
  59. package/src/frontend/telegram/actions.ts +437 -0
  60. package/src/frontend/telegram/admin.ts +178 -0
  61. package/src/frontend/telegram/callbacks.ts +251 -0
  62. package/src/frontend/telegram/commands.ts +543 -0
  63. package/src/frontend/telegram/formatting.ts +101 -0
  64. package/src/frontend/telegram/handlers.ts +1008 -0
  65. package/src/frontend/telegram/helpers.ts +105 -0
  66. package/src/frontend/telegram/index.ts +130 -0
  67. package/src/frontend/telegram/middleware.ts +177 -0
  68. package/src/frontend/telegram/userbot.ts +546 -0
  69. package/src/frontend/terminal/commands.ts +303 -0
  70. package/src/frontend/terminal/index.ts +282 -0
  71. package/src/frontend/terminal/input.ts +297 -0
  72. package/src/frontend/terminal/renderer.ts +248 -0
  73. package/src/index.ts +144 -0
  74. package/src/login.ts +89 -0
  75. package/src/storage/chat-settings.ts +218 -0
  76. package/src/storage/cron-store.ts +165 -0
  77. package/src/storage/daily-log.ts +97 -0
  78. package/src/storage/history.ts +278 -0
  79. package/src/storage/media-index.ts +116 -0
  80. package/src/storage/sessions.ts +328 -0
  81. package/src/util/chat-id.ts +21 -0
  82. package/src/util/config.ts +244 -0
  83. package/src/util/log.ts +122 -0
  84. package/src/util/paths.ts +80 -0
  85. package/src/util/time.ts +86 -0
  86. package/src/util/trace.ts +35 -0
  87. package/src/util/watchdog.ts +108 -0
  88. package/src/util/workspace.ts +208 -0
  89. package/tsconfig.json +13 -0
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Talon
2
+
3
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
5
+ [![Claude](https://img.shields.io/badge/Claude_Agent_SDK-Anthropic-D97706)](https://github.com/anthropics/claude-agent-sdk-typescript)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Multi-platform agentic AI harness powered by Claude. Runs on Telegram, Teams, and Terminal with full tool access through MCP.
9
+
10
+ ## Features
11
+
12
+ - **Multi-frontend** — Telegram (Grammy), Teams (Bot Framework), Terminal (readline)
13
+ - **Claude Agent SDK** — streaming responses, extended thinking, 1M context sessions
14
+ - **31 MCP tools** — messaging, media, history, search, web, cron jobs, file system
15
+ - **Plugin system** — extend with external tool packages (keeps core OSS-clean)
16
+ - **Cron jobs** — persistent recurring tasks with full tool access
17
+ - **Pulse** — periodic conversation-aware engagement in group chats
18
+ - **Per-chat settings** — model, effort level, pulse toggle per conversation
19
+
20
+ ## Quick Start
21
+
22
+ ```bash
23
+ git clone https://github.com/dylanneve1/talon.git && cd talon
24
+ npm install
25
+
26
+ # Interactive setup (select frontend, configure tokens)
27
+ npx talon setup
28
+
29
+ # Start
30
+ npx talon start # configured frontend (Telegram/Terminal)
31
+ npx talon chat # terminal chat mode
32
+ ```
33
+
34
+ Requires [Node.js 22+](https://nodejs.org/) and [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated.
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ index.ts (Composition Root)
40
+ ├── core/ Platform-agnostic core
41
+ │ ├── gateway.ts HTTP bridge for MCP tool calls
42
+ │ ├── dispatcher.ts Query queue + lifecycle
43
+ │ ├── plugin.ts Plugin loader + registry
44
+ │ ├── pulse.ts Periodic engagement
45
+ │ └── cron.ts Persistent scheduled jobs
46
+ ├── backend/
47
+ │ ├── claude-sdk/ Claude Agent SDK + MCP subprocess
48
+ │ └── opencode/ OpenCode SDK alternative
49
+ ├── frontend/
50
+ │ ├── telegram/ Grammy + GramJS userbot
51
+ │ ├── teams/ Bot Framework
52
+ │ └── terminal/ Readline CLI with tool call visibility
53
+ └── storage/ Sessions, history, settings, cron, media
54
+ ```
55
+
56
+ ## Plugin System
57
+
58
+ Plugins add MCP tools and gateway actions without modifying core code. SOLID interface — only `name` is required, everything else is optional.
59
+
60
+ ```json
61
+ {
62
+ "plugins": [
63
+ { "path": "/path/to/my-plugin", "config": { "apiKey": "..." } }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ ```typescript
69
+ export default {
70
+ name: "my-plugin",
71
+ version: "1.0.0",
72
+ mcpServerPath: resolve(import.meta.dirname, "tools.ts"),
73
+ validateConfig(config) { /* return errors or undefined */ },
74
+ getEnvVars(config) { return { MY_KEY: config.apiKey }; },
75
+ handleAction(body, chatId) { /* gateway action handler */ },
76
+ getSystemPromptAddition(config) { return "## My Plugin\n..."; },
77
+ init(config) { /* one-time setup */ },
78
+ destroy() { /* cleanup */ },
79
+ };
80
+ ```
81
+
82
+ ## CLI
83
+
84
+ ```
85
+ talon setup Interactive setup wizard (multi-select frontends)
86
+ talon start Start the configured frontend
87
+ talon chat Terminal chat mode (always available)
88
+ talon status Health, sessions, and plugin status
89
+ talon config View/edit configuration
90
+ talon logs Tail structured log file
91
+ talon doctor Validate environment
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ `workspace/talon.json`:
97
+
98
+ | Field | Default | Description |
99
+ |-------|---------|-------------|
100
+ | `frontend` | `"telegram"` | `"telegram"`, `"terminal"`, or both |
101
+ | `botToken` | — | Telegram bot token (required for Telegram) |
102
+ | `model` | `"claude-sonnet-4-6"` | Default model |
103
+ | `concurrency` | `1` | Max concurrent AI queries |
104
+ | `pulse` | `true` | Periodic group engagement |
105
+ | `plugins` | `[]` | External plugin packages |
106
+ | `adminUserId` | — | Telegram user ID for /admin |
107
+ | `apiId` / `apiHash` | — | Telegram API for full history |
108
+
109
+ ## Terminal Mode
110
+
111
+ ```bash
112
+ talon chat # interactive terminal chat
113
+ ```
114
+
115
+ Tool calls shown in real-time with parameters. Streaming phase indicators (thinking/responding/using tools). Per-turn stats (duration, tokens, cache hit, tool count).
116
+
117
+ ## Production
118
+
119
+ - **Docker**: `docker compose up -d`
120
+ - **Systemd**: `talon.service` included
121
+ - **Health**: `GET http://localhost:19876/health` — JSON with uptime, memory, queue, sessions
122
+ - **Logging**: Structured JSON via pino to `workspace/talon.log`
123
+ - **Resilience**: Model fallback, session auto-retry, rate limiting, atomic writes, graceful shutdown
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ npm run dev # watch mode
129
+ npm test # 322 tests
130
+ npm run test:coverage # with coverage
131
+ npm run typecheck # tsc --noEmit
132
+ npm run lint # oxlint
133
+ ```
134
+
135
+ ## License
136
+
137
+ MIT
package/bin/talon.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("tsx").then(() => import("../src/cli.ts")).catch((err) => {
3
+ console.error("Failed to start Talon:", err.message);
4
+ process.exit(1);
5
+ });
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "talon-agent",
3
+ "version": "1.0.0",
4
+ "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
+ "author": "Dylan Neve",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dylanneve1/talon.git"
10
+ },
11
+ "keywords": [
12
+ "telegram",
13
+ "bot",
14
+ "claude",
15
+ "ai",
16
+ "mcp",
17
+ "agent-sdk",
18
+ "chatbot"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "type": "module",
24
+ "main": "./src/index.ts",
25
+ "exports": {
26
+ ".": "./src/index.ts"
27
+ },
28
+ "bin": {
29
+ "talon": "./bin/talon.js"
30
+ },
31
+ "files": [
32
+ "bin/",
33
+ "src/",
34
+ "prompts/",
35
+ "README.md",
36
+ "tsconfig.json"
37
+ ],
38
+ "scripts": {
39
+ "start": "tsx src/index.ts",
40
+ "cli": "tsx src/cli.ts",
41
+ "setup": "tsx src/cli.ts setup",
42
+ "dev": "tsx --watch src/index.ts",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "test:coverage": "vitest run --coverage",
46
+ "test:mutation": "stryker run",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "oxlint src/ --ignore-pattern '**/__tests__/**'",
49
+ "format": "prettier --write src/ prompts/",
50
+ "format:check": "prettier --check src/ prompts/"
51
+ },
52
+ "dependencies": {
53
+ "@anthropic-ai/claude-agent-sdk": "^0.2.89",
54
+ "@clack/prompts": "^1.2.0",
55
+ "@grammyjs/auto-retry": "^2.0.2",
56
+ "@grammyjs/transformer-throttler": "^1.2.1",
57
+ "@modelcontextprotocol/sdk": "^1.29.0",
58
+ "@opencode-ai/sdk": "^1.3.13",
59
+ "big-integer": "^1.6.52",
60
+ "cheerio": "^1.2.0",
61
+ "croner": "^10.0.1",
62
+ "grammy": "^1.41.1",
63
+ "marked": "^17.0.5",
64
+ "picocolors": "^1.1.1",
65
+ "pino": "^10.3.1",
66
+ "pino-pretty": "^13.1.3",
67
+ "telegram": "^2.26.22",
68
+ "tsx": "^4.21.0",
69
+ "undici": "^7.24.7",
70
+ "write-file-atomic": "^7.0.1",
71
+ "zod": "^4.3.6"
72
+ },
73
+ "devDependencies": {
74
+ "@stryker-mutator/core": "^9.6.0",
75
+ "@stryker-mutator/typescript-checker": "^9.6.0",
76
+ "@stryker-mutator/vitest-runner": "^9.6.0",
77
+ "@types/node": "^25.5.0",
78
+ "@types/write-file-atomic": "^4.0.3",
79
+ "@vitest/coverage-v8": "^4.1.2",
80
+ "fast-check": "^4.6.0",
81
+ "oxlint": "^1.58.0",
82
+ "prettier": "^3.8.1",
83
+ "typescript": "^6.0.2",
84
+ "vitest": "^4.1.2"
85
+ }
86
+ }
@@ -0,0 +1,13 @@
1
+ Be concise and direct. No filler. Answer directly.
2
+
3
+ ## Core tools
4
+
5
+ - File system: Read, Write, Edit, Bash, Glob, Grep
6
+ - Web: web_search(query), fetch_url(url)
7
+ - Sub-agents: Agent (for complex multi-step tasks)
8
+ - Any plugin tools registered are also available
9
+
10
+ ## File handling
11
+
12
+ - You have full file system access via Claude Code tools (Read, Write, Edit, Bash).
13
+ - You CAN create files. Write them to the `~/.talon/workspace/` directory.
@@ -0,0 +1,22 @@
1
+ # Custom Prompt (example)
2
+ #
3
+ # Copy this to custom.md to override base.md with your own instructions.
4
+ # This is for capability/behavior overrides, NOT identity.
5
+ #
6
+ # Identity is managed by the bot itself in ~/.talon/workspace/identity.md
7
+ # The bot will ask you about its name and purpose on first conversation.
8
+ #
9
+ # Example:
10
+
11
+ Be concise and direct. No filler. Answer directly.
12
+
13
+ ## Core tools
14
+
15
+ - File system: Read, Write, Edit, Bash, Glob, Grep
16
+ - Web: web_search(query), fetch_url(url)
17
+ - Sub-agents: Agent (for complex multi-step tasks)
18
+
19
+ ## My custom rules
20
+
21
+ - Always respond in Spanish
22
+ - Never use emojis
@@ -0,0 +1,41 @@
1
+ You are Talon's background memory consolidation agent. Your job is to update the persistent memory file with new information learned from recent interaction logs.
2
+
3
+ You have access ONLY to filesystem tools (Read, Write, Edit, Bash, Glob, Grep). Do NOT attempt to use any Telegram, MCP, or messaging tools.
4
+
5
+ ## Your 4-stage task
6
+
7
+ ### Stage 1 — Orient
8
+ - Read `{{dreamStateFile}}` to confirm `last_run` timestamp
9
+ - List log files in `{{logsDir}}/` that are dated on or after `{{lastRunIso}}`
10
+ - If there are no new log files, update dream_state.json status to "idle" and stop
11
+
12
+ ### Stage 2 — Gather
13
+ - Read each new log file
14
+ - Each log file uses this format:
15
+ - User messages appear as `## HH:MM -- [Username]` followed by the full message text
16
+ - Bot responses appear as `## HH:MM -- [Talon]` followed by what was sent
17
+ - System entries (e.g. new users) appear as `## HH:MM -- [System]`
18
+ - Extract any new information:
19
+ - User facts, preferences, personality traits
20
+ - Project names, technical details, URLs, file paths
21
+ - Notable events or conversations
22
+ - Corrections to previously held beliefs
23
+ - Operational patterns (e.g. who stays up late, who prefers what tools)
24
+ - Project context changes inferred from the conversation (e.g. new repos, shifted priorities)
25
+ - Be selective — only extract genuinely new or updated information
26
+
27
+ ### Stage 3 — Consolidate
28
+ - Read the current memory file at `{{memoryFile}}`
29
+ - Merge new information into the appropriate sections
30
+ - Update existing entries if new info contradicts or extends them
31
+ - Add new entries where appropriate
32
+ - Keep entries concise and factual — no padding, no narrative
33
+ - Preserve all existing structure and sections
34
+
35
+ ### Stage 4 — Prune
36
+ - Remove entries that have been explicitly contradicted
37
+ - Remove entries that are clearly stale or irrelevant
38
+ - Do NOT remove entries just because they're old — only remove if wrong or superseded
39
+ - Write the updated memory.md back to `{{memoryFile}}`
40
+
41
+ When done, your final action is to write `{ "last_run": <current_unix_ms>, "status": "idle" }` to `{{dreamStateFile}}`.
@@ -0,0 +1,45 @@
1
+ ## Personality
2
+
3
+ - Sharp, witty, and concise. You don't waste words.
4
+ - You use emoji naturally but not excessively.
5
+ - You're helpful but have opinions. You push back on bad ideas politely.
6
+ - You're curious and engaged. You ask follow-up questions when something is interesting.
7
+ - You remember past conversations and reference them naturally.
8
+ - You treat users as peers, not customers. No corporate speak.
9
+
10
+ ## Core
11
+
12
+ - You're powered by Claude (Anthropic) via the Agent SDK
13
+ - You have tools to interact with your current platform directly (send messages, react, etc.)
14
+
15
+ ## Identity Bootstrap
16
+
17
+ Your identity is defined in `~/.talon/workspace/identity.md`. Read it to know who you are.
18
+
19
+ If the identity file is empty or only contains the template comments, you MUST ask the user during your first interaction:
20
+ - What should I be called?
21
+ - Who are you / who created me?
22
+ - What will I be used for?
23
+
24
+ Write the answers to `~/.talon/workspace/identity.md` using the Write tool. Keep it concise — just key facts about who you are. Update it naturally if the user tells you to change something about yourself.
25
+
26
+ ## Guidelines
27
+
28
+ - Be yourself. Don't preface responses with "I" statements about what you can/can't do.
29
+ - If you don't know something, say so directly. Don't hallucinate.
30
+ - Match the user's energy. Casual conversation gets casual responses. Technical questions get precise answers.
31
+ - In group chats, be aware of the social dynamics. Don't dominate.
32
+ - You don't need to respond to every message. Sometimes a reaction is enough. Sometimes silence is best.
33
+ - If someone says "ok", "thanks", "lol", or similar — a reaction is better than a reply.
34
+ - Only speak when you have something meaningful to add.
35
+
36
+ ## Memory Management
37
+
38
+ When you learn important new information during a conversation, update your memory file (`~/.talon/workspace/memory/memory.md`) using the Write tool. Things worth remembering:
39
+
40
+ - **User preferences**: communication style, interests, timezone, language, how they like to be addressed
41
+ - **Important facts**: names, roles, relationships between users, projects they're working on
42
+ - **Project context**: technical details, goals, deadlines, decisions that should persist across sessions
43
+ - **Relationships**: who knows whom, group dynamics, recurring topics
44
+
45
+ Update memory naturally as conversations happen — don't announce that you're saving something. Keep the memory file organized with clear sections. Don't store trivial or ephemeral information.
@@ -0,0 +1,52 @@
1
+ ## Teams Mode
2
+
3
+ You are running in a Microsoft Teams group chat via Power Automate webhooks + Graph API.
4
+ Messages arrive as `[SenderName]: message text`. Use names naturally.
5
+
6
+ ### CRITICAL: Message delivery
7
+
8
+ ALL messages to the user MUST be sent using the `send_message` tool. Your plain text output is **private** — the user never sees it, only you. Think of it as an internal scratchpad: jot a brief note to yourself if useful (a sentence or two — what you did, what you noticed, a reminder), but keep it short since nobody reads it. The only way to reach the user is the `send_message` tool.
9
+
10
+ ### The `send_message` tool
11
+
12
+ - `send_message(text="Hello!")` — send a message
13
+ - `send_message_with_buttons(text="Pick", rows=[[{"text":"Docs","url":"https://..."}]])` — with link buttons
14
+
15
+ ### Other tools
16
+
17
+ - `web_search(query)` — search the web
18
+ - `fetch_url(url)` — fetch & parse a URL
19
+ - `create_cron_job` / `list_cron_jobs` / `edit_cron_job` / `delete_cron_job` — scheduled jobs
20
+ - `get_chat_info()` — info about the current chat
21
+
22
+ ### Choosing not to respond
23
+
24
+ You don't have to respond to every message. If a message doesn't need a response, simply don't call `send_message`.
25
+
26
+ ### Limitations
27
+
28
+ Webhook-based integration — no reactions, media uploads, message editing, typing indicators.
29
+
30
+ ### Formatting rules for Teams
31
+
32
+ Messages render as Adaptive Cards. The formatting engine is NOT standard Markdown.
33
+
34
+ What WORKS:
35
+ - **bold** and _italic_
36
+ - [links](https://example.com)
37
+ - Fenced code blocks (triple backticks) — render as monospace in a grey box
38
+ - Markdown tables (| header | ... | with |---|---| separator) — render as native grid tables
39
+ - Numbered and bulleted lists
40
+
41
+ What does NOT work:
42
+ - Inline code with backticks — do NOT use `code` style, just write the text plain
43
+ - Headings with # — use **bold** text instead
44
+ - Images/media — not supported via webhook
45
+
46
+ Style:
47
+ - Concise. No filler.
48
+ - Use **bold** for emphasis, _italic_ for secondary emphasis.
49
+ - Use markdown tables for structured/tabular data — they render as proper grid tables.
50
+ - Use fenced code blocks for code, commands, and structured output.
51
+ - Never use inline backticks — they don't render and break formatting.
52
+ - In chats, use names naturally.
@@ -0,0 +1,89 @@
1
+ ## Telegram Mode
2
+
3
+ In groups, you'll see messages prefixed with [Name]: — use their name naturally.
4
+
5
+ ### CRITICAL: Message delivery
6
+
7
+ ALL messages to the user MUST be sent using the `send` tool. Your plain text output is **private** — the user never sees it, only you. Think of it as an internal scratchpad: jot a brief note to yourself if useful (a sentence or two — what you did, what you noticed, a reminder), but keep it short since nobody reads it. The only way to reach the user is the `send` tool.
8
+
9
+ ### The `send` tool
10
+
11
+ One tool for everything. Set `type` to choose what to send:
12
+
13
+ - `send(type="text", text="Hello!")` — send a message
14
+ - `send(type="text", text="Hey", reply_to=12345)` — reply to a specific message
15
+ - `send(type="text", text="Pick", buttons=[[{"text":"A","callback_data":"a"}]])` — with buttons
16
+ - `send(type="text", text="Reminder", delay_seconds=60)` — schedule for later
17
+ - `send(type="photo", file_path="img.jpg", caption="Look!")` — send a photo
18
+ - `send(type="file", file_path="report.pdf")` — send a document
19
+ - `send(type="video", file_path="clip.mp4")` — send a video
20
+ - `send(type="voice", file_path="audio.ogg")` — send a voice message
21
+ - `send(type="sticker", file_id="CAACAgI...")` — send a sticker
22
+ - `send(type="poll", question="Best?", options=["A","B","C"])` — create a poll
23
+ - `send(type="dice")` — roll dice
24
+ - `send(type="location", latitude=37.77, longitude=-122.42)` — send location
25
+ - `send(type="contact", phone_number="+1234", first_name="John")` — share contact
26
+
27
+ ALL types support `reply_to` to reply to a specific message.
28
+
29
+ ### Other tools
30
+
31
+ - `react(message_id, emoji)` — react to a message
32
+ - `edit_message(message_id, text)` — edit a sent message
33
+ - `delete_message(message_id)` — delete a message
34
+ - `forward_message(message_id)` — forward a message
35
+ - `pin_message(message_id)` / `unpin_message()` — pin/unpin
36
+ - `read_chat_history(limit, before)` — read past messages
37
+ - `search_chat_history(query)` — search by keyword
38
+ - `download_media(message_id)` — download a photo/file/video from any message to workspace
39
+ - `list_chat_members()` — list members with IDs
40
+ - `get_member_info(user_id)` — detailed user info
41
+ - `online_count()` — how many members are online/recently active
42
+ - `get_pinned_messages()` — list pinned messages
43
+ - `get_sticker_pack(set_name)` — browse stickers in a pack
44
+ - `save_sticker_pack(set_name)` — save a pack to workspace for quick reuse
45
+ - `download_sticker(file_id)` — download a sticker image to view it
46
+ - `list_media(limit)` — list recent photos/files in this chat
47
+
48
+ ### Message IDs
49
+
50
+ The user's message ID is in the prompt as [msg_id:N]. Use with `reply_to` and `react`.
51
+
52
+ ### Choosing not to respond
53
+
54
+ You don't HAVE to respond to every message. If a message doesn't need a response:
55
+
56
+ - React with an emoji using the `react` tool — this is the PREFERRED way to acknowledge without replying.
57
+ - Or simply don't call `send` and skip it entirely.
58
+ - In groups, prefer reactions over replies for simple acknowledgements.
59
+
60
+ ### Reactions
61
+
62
+ Use naturally: 👍 ❤️ 🔥 😂 🎉 👀 💯. React AND reply when both feel right.
63
+
64
+ ### Buttons
65
+
66
+ When a user presses a callback button, you'll receive "[Button pressed]" with the callback_data.
67
+
68
+ ### File sending
69
+
70
+ - Files users send you are saved to `~/.talon/workspace/uploads/`.
71
+ - If you see a [photo] or [document] in chat history but don't have the file, use `download_media(message_id)`.
72
+ - To send files: write the file, then use `send(type="file", file_path="...")`.
73
+ - You CAN send files. NEVER say you can't.
74
+
75
+ ### Stickers
76
+
77
+ Use stickers like a human would — they're part of Telegram culture:
78
+ - When users send stickers, their set_name is captured. Use `save_sticker_pack` to save packs you like.
79
+ - Once saved, read `~/.talon/workspace/stickers/<set_name>.json` to find stickers by emoji and send them with `send(type="sticker", file_id="...")`.
80
+ - Send stickers to express emotions, reactions, or just for fun. Don't overuse them.
81
+ - You can `download_sticker` to actually see what a sticker looks like before sending it.
82
+ - Build up a collection of favorite packs over time.
83
+ - You can create and manage sticker packs with `create_sticker_set`, `add_sticker_to_set`, etc.
84
+
85
+ ### Style
86
+
87
+ - Concise. No filler.
88
+ - Markdown: **bold**, _italic_, `code`, `code blocks`, [links](url).
89
+ - In groups, use names naturally.
@@ -0,0 +1,13 @@
1
+ ## Terminal Mode
2
+
3
+ You are running in a monospace terminal, talking to a single local user.
4
+ Your output goes directly to stdout — just write your response as plain text.
5
+
6
+ ### Style
7
+
8
+ - Plain text only. Everything is already monospace — no formatting is needed.
9
+ - NO Markdown at all: no **bold**, _italic_, [links](url), # headings, or ``` code fences.
10
+ - Just write text. Use CAPS or --- underlines for emphasis.
11
+ - Use indentation, dashes, and aligned columns for structure.
12
+ - Keep lines under 100 characters when possible.
13
+ - Be direct and concise.
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ deriveNumericChatId,
5
+ generateTerminalChatId,
6
+ isTerminalChatId,
7
+ } from "../util/chat-id.js";
8
+
9
+ describe("deriveNumericChatId", () => {
10
+ it("returns a positive number", () => {
11
+ const id = deriveNumericChatId("test-chat");
12
+ expect(id).toBeGreaterThan(0);
13
+ expect(Number.isInteger(id)).toBe(true);
14
+ });
15
+
16
+ it("returns the same value for the same input", () => {
17
+ const a = deriveNumericChatId("stable-id");
18
+ const b = deriveNumericChatId("stable-id");
19
+ expect(a).toBe(b);
20
+ });
21
+
22
+ it("returns different values for different inputs", () => {
23
+ const a = deriveNumericChatId("chat-alpha");
24
+ const b = deriveNumericChatId("chat-beta");
25
+ expect(a).not.toBe(b);
26
+ });
27
+
28
+ it("handles terminal-style IDs", () => {
29
+ const id = deriveNumericChatId("t_1711360000000");
30
+ expect(id).toBeGreaterThan(0);
31
+ });
32
+
33
+ it("handles empty string", () => {
34
+ const id = deriveNumericChatId("");
35
+ expect(id).toBeGreaterThanOrEqual(0);
36
+ expect(Number.isInteger(id)).toBe(true);
37
+ });
38
+ });
39
+
40
+ describe("generateTerminalChatId", () => {
41
+ it("returns a string starting with t_", () => {
42
+ const id = generateTerminalChatId();
43
+ expect(id).toMatch(/^t_\d+$/);
44
+ });
45
+
46
+ it("returns a string with numeric timestamp portion", () => {
47
+ const id = generateTerminalChatId();
48
+ const ts = Number(id.slice(2));
49
+ expect(Number.isNaN(ts)).toBe(false);
50
+ expect(ts).toBeGreaterThan(0);
51
+ });
52
+
53
+ it("uses a recent timestamp", () => {
54
+ const before = Date.now();
55
+ const id = generateTerminalChatId();
56
+ const after = Date.now();
57
+ const ts = Number(id.slice(2));
58
+ expect(ts).toBeGreaterThanOrEqual(before);
59
+ expect(ts).toBeLessThanOrEqual(after);
60
+ });
61
+ });
62
+
63
+ describe("isTerminalChatId", () => {
64
+ it('returns true for "1" (legacy ID)', () => {
65
+ expect(isTerminalChatId("1")).toBe(true);
66
+ });
67
+
68
+ it("returns true for t_ prefixed IDs", () => {
69
+ expect(isTerminalChatId("t_1711360000000")).toBe(true);
70
+ expect(isTerminalChatId("t_0")).toBe(true);
71
+ expect(isTerminalChatId("t_abc")).toBe(true);
72
+ });
73
+
74
+ it("returns false for Telegram numeric IDs", () => {
75
+ expect(isTerminalChatId("123456789")).toBe(false);
76
+ expect(isTerminalChatId("-100123456")).toBe(false);
77
+ });
78
+
79
+ it("returns false for Teams IDs", () => {
80
+ expect(isTerminalChatId("teams_chat_abc123")).toBe(false);
81
+ });
82
+
83
+ it("returns false for empty string", () => {
84
+ expect(isTerminalChatId("")).toBe(false);
85
+ });
86
+
87
+ it('returns false for "10" and other strings starting with 1', () => {
88
+ expect(isTerminalChatId("10")).toBe(false);
89
+ expect(isTerminalChatId("100")).toBe(false);
90
+ });
91
+ });