Skip to content
Merged
6 changes: 3 additions & 3 deletions .agent-core/agent-core.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
"type": "remote",
"url": "https://mcp.context7.com/mcp"
},
"personas-memory": {
"memory": {
"type": "local",
"command": ["bun", "run", "/home/artur/.local/src/agent-core/src/mcp/servers/memory.ts"],
"enabled": true
},
"personas-calendar": {
"calendar": {
"type": "local",
"command": ["bun", "run", "/home/artur/.local/src/agent-core/src/mcp/servers/calendar.ts"],
"enabled": true
},
"personas-portfolio": {
"portfolio": {
"type": "local",
"command": ["bun", "run", "/home/artur/.local/src/agent-core/src/mcp/servers/portfolio.ts"],
"enabled": true
Expand Down
74 changes: 16 additions & 58 deletions .agent-core/tool/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,25 @@ Examples:
config: tool.schema.string().describe("JSON configuration for the canvas content"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")

let config: Record<string, unknown>
try {
config = JSON.parse(args.config)
} catch {
return `Invalid JSON config: ${args.config}`
}

try {
const result = await requestDaemon<{ paneId: string; id: string; kind: string }>(
"canvas:spawn",
{ kind: args.kind, id: args.id, config }
)
return `Canvas "${args.id}" (${args.kind}) displayed in pane ${result.paneId}.
// Canvas daemon integration not yet implemented
// For now, return the content as formatted text
const title = (config.title as string) || args.id
const content = (config.content as string) || JSON.stringify(config, null, 2)

Content:
${JSON.stringify(config, null, 2)}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to spawn canvas: ${msg}
return `=== ${title} ===
(Canvas type: ${args.kind})

Note: Canvas requires the agent-core daemon to be running.
Start it with: agent-core daemon`
}
${content}

---
Note: WezTerm canvas panes are not yet implemented. Content displayed inline.`
},
})

Expand All @@ -78,25 +72,18 @@ Examples:
config: tool.schema.string().describe("New JSON configuration"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")

let config: Record<string, unknown>
try {
config = JSON.parse(args.config)
} catch {
return `Invalid JSON config: ${args.config}`
}

try {
await requestDaemon("canvas:update", { id: args.id, config })
return `Canvas "${args.id}" updated.
// Canvas daemon integration not yet implemented
return `Canvas "${args.id}" update requested (not yet implemented).

New content:
${JSON.stringify(config, null, 2)}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to update canvas: ${msg}`
}
},
})

Expand All @@ -107,15 +94,8 @@ export const canvasClose = tool({
id: tool.schema.string().describe("Canvas identifier to close"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")

try {
await requestDaemon("canvas:close", { id: args.id })
return `Canvas "${args.id}" closed.`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to close canvas: ${msg}`
}
// Canvas daemon integration not yet implemented
return `Canvas "${args.id}" close requested (not yet implemented).`
},
})

Expand All @@ -124,29 +104,7 @@ export const canvasList = tool({
description: `List all active canvases.`,
args: {},
async execute() {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")

try {
const canvases = await requestDaemon<
Array<{
id: string
kind: string
paneId: string
createdAt: number
}>
>("canvas:list", {})

if (canvases.length === 0) {
return "No active canvases."
}

const list = canvases.map((c) => `- ${c.id} (${c.kind}) in pane ${c.paneId}`).join("\n")

return `${canvases.length} active canvas(es):
${list}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to list canvases: ${msg}`
}
// Canvas daemon integration not yet implemented
return `Canvas listing not yet implemented. WezTerm canvas panes are a planned feature.`
},
})
30 changes: 16 additions & 14 deletions .agent-core/tool/zee-messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ export default tool({
description: `Send messages via WhatsApp or Telegram gateways.

Channels:
- **whatsapp**: Zee's WhatsApp gateway (requires active daemon with --whatsapp)
- **telegram**: Stanley/Johny Telegram bots (requires active daemon with --telegram-*)
- **whatsapp**: Zee's WhatsApp gateway (requires agent-core daemon with gateway enabled)
- **telegram**: Telegram bots (requires agent-core daemon with gateway enabled)

WhatsApp:
- \`to\`: Chat ID (from incoming message context, e.g., "1234567890@c.us")
- to: E164 phone (e.g., "+1555...") or chat JID (e.g., "1234567890@c.us" or "...@g.us")
- Only Zee can send via WhatsApp

Telegram:
- \`to\`: Numeric chat ID (from incoming message context)
- \`persona\`: Which bot to use - "stanley" (default) or "johny"
- to: Chat ID (numeric) or @username
Comment on lines 20 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit Telegram docs to numeric IDs or accept @username

These lines now advertise Telegram to as “Chat ID (numeric) or @username”, but the execution path still does parseInt(to, 10) and treats non‑numeric IDs as invalid. When a user follows this new guidance and supplies an @username, the tool returns “Invalid Telegram chat ID” and never calls /gateway/telegram/send. Either keep the description numeric‑only or accept string usernames and forward them to the gateway.

Useful? React with 👍 / 👎.

- persona: Which bot/account to use - "stanley" (default) or "johny"

Examples:
- WhatsApp: { channel: "whatsapp", to: "1234567890@c.us", message: "Hello!" }
- WhatsApp: { channel: "whatsapp", to: "+15551234567", message: "Hello!" }
- Telegram via Stanley: { channel: "telegram", to: "123456789", message: "Market update!", persona: "stanley" }`,
args: {
channel: tool.schema
Expand All @@ -38,9 +38,11 @@ Examples:
async execute(args) {
const { channel, to, message, persona } = args

// Get daemon port from environment or default
const daemonPort = process.env.AGENT_CORE_DAEMON_PORT || "3456"
const baseUrl = `http://127.0.0.1:${daemonPort}`
const rawBaseUrl =
process.env.AGENT_CORE_URL ||
process.env.AGENT_CORE_DAEMON_URL ||
`http://127.0.0.1:${process.env.AGENT_CORE_PORT || "3210"}`
const baseUrl = rawBaseUrl.replace(/\/$/, "")

try {
if (channel === "whatsapp") {
Expand All @@ -56,9 +58,9 @@ Examples:
return `Failed to send WhatsApp message: ${error}

Troubleshooting:
- Ensure daemon is running with --whatsapp flag
- Check WhatsApp connection status
- Verify chatId format (e.g., "1234567890@c.us")`
- Ensure \`agent-core daemon\` is running
- Check \`agent-core debug status\` shows Gateway: Active
- Verify recipient format (E164 like "+1555..." or JID like "1234567890@c.us")`
}

const result = await response.json()
Expand Down Expand Up @@ -91,8 +93,8 @@ Chat ID must be a numeric value (e.g., 123456789).`
return `Failed to send Telegram message via ${selectedPersona}: ${error}

Troubleshooting:
- Ensure daemon is running with --telegram-${selectedPersona}-token flag
- Check bot connection status
- Ensure \`agent-core daemon\` is running
- Check \`agent-core debug status\` shows Gateway: Active
- Verify chatId is numeric`
}

Expand Down
33 changes: 33 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Testing

## Automated (Non-UI)

Run from repo root:

```bash
cd packages/agent-core
bun test
bun run typecheck
```

## Manual (TUI / UI)

Run from repo root:

```bash
cd packages/agent-core
bun dev
```

Smoke checklist:

- TUI launches and renders without crashing.
- `Ctrl+X H` toggles `HOLD`/`RELEASE` mode.
- `Ctrl+T` cycles model variants (for models that define variants).
- Provider dialog accepts an API key and shows success toast.

## Latest Run (2026-01-17)

- `cd packages/agent-core && bun test` (pass)
- `cd packages/agent-core && bun run typecheck` (pass)
- `cd packages/agent-core && bun dev` (launched TUI)
124 changes: 74 additions & 50 deletions packages/agent-core/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,28 +228,37 @@ export const AuthLoginCommand = cmd({
async fn() {
UI.empty()
prompts.intro("Add credential")
if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
stdout: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
const rawInput = typeof args.url === "string" ? args.url.trim() : ""
let providerArg: string | undefined
if (rawInput) {
try {
const url = new URL(rawInput)
const wellknown = await fetch(`${url.toString().replace(/\/$/, "")}/.well-known/opencode`).then(
(x) => x.json() as any,
)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
stdout: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await new Response(proc.stdout).text()
await Auth.set(url.toString(), {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
prompts.log.success("Logged into " + url.toString())
prompts.outro("Done")
return
} catch {
providerArg = rawInput
}
const token = await new Response(proc.stdout).text()
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
prompts.log.success("Logged into " + args.url)
prompts.outro("Done")
return
}
await ModelsDev.refresh().catch(() => {})

Expand Down Expand Up @@ -277,35 +286,51 @@ export const AuthLoginCommand = cmd({
openrouter: 5,
vercel: 6,
}
let provider = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
let provider = providerArg ?? ""
if (!provider) {
const selected = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: {
opencode: "recommended",
anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
}[x.id],
})),
),
map((x) => ({
label: x.name,
value: x.id,
hint: {
opencode: "recommended",
anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
}[x.id],
})),
),
{
value: "other",
label: "Other",
},
],
})
{
value: "other",
label: "Other",
},
],
})
if (prompts.isCancel(selected)) throw new UI.CancelledError()
provider = selected as string
}

if (prompts.isCancel(provider)) throw new UI.CancelledError()
const knownProvider = provider in providers
if (!knownProvider && provider !== "other") {
provider = provider.replace(/^@ai-sdk\//, "")
const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
if (customPlugin && customPlugin.auth) {
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider)
if (handled) return
}
prompts.log.warn(
`This only stores a credential for ${provider} - you will need configure it in agent-core.json, check the docs for examples.`,
)
}

const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
if (plugin && plugin.auth) {
Expand All @@ -314,13 +339,12 @@ export const AuthLoginCommand = cmd({
}

if (provider === "other") {
provider = await prompts.text({
const entered = await prompts.text({
message: "Enter provider id",
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
if (prompts.isCancel(provider)) throw new UI.CancelledError()
if (prompts.isCancel(entered)) throw new UI.CancelledError()
provider = entered.replace(/^@ai-sdk\//, "")

// Check if a plugin provides auth for this custom provider
const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
Expand Down
Loading
Loading