Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions agent/tool_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,37 @@ def _execute(next_args: dict) -> Any:
duration_ms=int(tool_duration * 1000),
middleware_trace=list(middleware_trace),
)
# Agent-runtime tools also never reach handle_function_call, which is
# the sole call site of transform_tool_result for registry-dispatched
# tools. Fire it here for the same set of inline-dispatched tools so
# the hook applies to every tool as documented.
if not _execution_blocked and agent_runtime_owns_post_tool_hook(agent, function_name):
try:
from hermes_cli.plugins import has_hook, invoke_hook
if has_hook("transform_tool_result"):
from model_tools import _tool_result_observer_fields
_tr_status, _tr_err_type, _tr_err_msg = _tool_result_observer_fields(function_result)
_tr_results = invoke_hook(
"transform_tool_result",
tool_name=function_name,
args=function_args,
result=function_result,
task_id=effective_task_id or "",
session_id=agent.session_id or "",
tool_call_id=getattr(tool_call, "id", "") or "",
turn_id=getattr(agent, "_current_turn_id", "") or "",
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
duration_ms=int(tool_duration * 1000),
status=_tr_status,
error_type=_tr_err_type,
error_message=_tr_err_msg,
)
for _tr_hook_result in _tr_results:
if isinstance(_tr_hook_result, str):
function_result = _tr_hook_result
break
except Exception as _tr_hook_err:
logger.debug("transform_tool_result hook error: %s", _tr_hook_err)
if not _execution_blocked:
function_result = agent._append_guardrail_observation(
function_name,
Expand Down
81 changes: 36 additions & 45 deletions skills/email/himalaya/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: himalaya
description: "Himalaya CLI: IMAP/SMTP email from terminal."
version: 1.1.0
version: 2.0.0
author: community
license: MIT
platforms: [linux, macos, windows]
Expand Down Expand Up @@ -48,70 +48,62 @@ cargo install himalaya --locked

## Configuration Setup

Run the interactive wizard to set up an account:
Run the interactive wizard (bare `himalaya` — no subcommand) to set up an account:

```bash
himalaya account configure
himalaya
```

Or create `~/.config/himalaya/config.toml` manually:
The wizard tests IMAP and SMTP connectivity, then prints a ready-to-save
TOML config to stdout. Redirect it directly:

```bash
himalaya > ~/.config/himalaya/config.toml
```

Or create `~/.config/himalaya/config.toml` manually using per-backend tables:

```toml
[accounts.personal]
email = "you@example.com"
display-name = "Your Name"
default = true

backend.type = "imap"
backend.host = "imap.example.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "you@example.com"
backend.auth.type = "password"
backend.auth.cmd = "pass show email/imap" # or use keyring

message.send.backend.type = "smtp"
message.send.backend.host = "smtp.example.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "you@example.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "pass show email/smtp"

# Folder aliases (himalaya v1.2.0+ syntax). Required whenever the
# server's folder names don't match himalaya's canonical names
# (inbox/sent/drafts/trash). Gmail is the common case — see
# `references/configuration.md` for the `[Gmail]/Sent Mail` mapping.
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
imap.server = "imap.example.com:993"
imap.sasl.plain.username = "you@example.com"
imap.sasl.plain.password.raw = "your-password"

smtp.server = "smtp.example.com:587"
smtp.starttls = true
smtp.sasl.plain.username = "you@example.com"
smtp.sasl.plain.password.raw = "your-password"

mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "Sent"
mailbox.alias.drafts = "Drafts"
mailbox.alias.trash = "Trash"
```

> **Heads up on the alias syntax.** Pre-v1.2.0 docs used a
> `[accounts.NAME.folder.alias]` sub-section (singular `alias`).
> v1.2.0 silently ignores that form — TOML parses fine, but the
> alias resolver never reads it, so every lookup falls through to
> the canonical name. On Gmail this means save-to-Sent fails *after*
> SMTP delivery succeeds, and `himalaya message send` exits non-zero.
> Any caller (agent, script, user) that retries on that exit code
> will re-run the entire send — including SMTP — producing duplicate
> emails to recipients. Always use `folder.aliases.X` (plural, dotted
> keys, directly under `[accounts.NAME]`).
> **Heads up on the alias syntax.** Himalaya v2.0.0 renamed
> `folder.aliases.*` to `mailbox.alias.*`. The old dotted keys are
> silently ignored — TOML parses fine, but the alias resolver never reads
> them. On Gmail this means save-to-Sent fails *after* SMTP delivery
> succeeds, and `himalaya message send` exits non-zero. Always use
> `mailbox.alias.X` in v2.0.0+.

## Hermes Integration Notes

- **Reading, listing, searching, moving, deleting** all work directly through the terminal tool
- **Composing/replying/forwarding** — piped input (`cat << EOF | himalaya template send`) is recommended for reliability. Interactive `$EDITOR` mode works with `pty=true` + background + process tool, but requires knowing the editor and its commands
- Use `--output json` for structured output that's easier to parse programmatically
- The `himalaya account configure` wizard requires interactive input — use PTY mode: `terminal(command="himalaya account configure", pty=true)`
- Use `--json` before the subcommand for structured output that's easier to parse programmatically (e.g. `himalaya --json envelope list`)
- The bare `himalaya` wizard requires interactive input — use PTY mode: `terminal(command="himalaya", pty=true)`

## Common Operations

### List Folders
### List Mailboxes

```bash
himalaya folder list
himalaya mailbox list
```

### List Emails
Expand Down Expand Up @@ -275,11 +267,10 @@ himalaya attachment download 42 --downloads-dir ~/Downloads

## Output Formats

Most commands support `--output` for structured output:
Most commands support `--json` (before the subcommand) for structured output:

```bash
himalaya envelope list --output json
himalaya envelope list --output plain
himalaya --json envelope list
```

## Debugging
Expand Down
156 changes: 60 additions & 96 deletions skills/email/himalaya/references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,41 @@ display-name = "Your Name"
default = true

# IMAP backend for reading emails
backend.type = "imap"
backend.host = "imap.example.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "user@example.com"
backend.auth.type = "password"
backend.auth.raw = "your-password"
imap.server = "imap.example.com:993"
imap.sasl.plain.username = "user@example.com"
imap.sasl.plain.password.raw = "your-password"

# SMTP backend for sending emails
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.example.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "user@example.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "your-password"

# Folder aliases — required whenever server folder names differ
# from himalaya's canonical names. See "Folder Aliases" below.
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
smtp.server = "smtp.example.com:587"
smtp.starttls = true
smtp.sasl.plain.username = "user@example.com"
smtp.sasl.plain.password.raw = "your-password"

# Mailbox aliases — required whenever server folder names differ
# from himalaya's canonical names. See "Mailbox Aliases" below.
mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "Sent"
mailbox.alias.drafts = "Drafts"
mailbox.alias.trash = "Trash"
```

## Password Options

### Raw password (testing only, not recommended)

```toml
backend.auth.raw = "your-password"
imap.sasl.plain.password.raw = "your-password"
# smtp.sasl.plain.password.raw = "your-password"
```

### Password from command (recommended)

```toml
backend.auth.cmd = "pass show email/imap"
# backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w"
imap.sasl.plain.password.cmd = "pass show email/imap"
# imap.sasl.plain.password.cmd = "security find-generic-password -a user@example.com -s imap -w"
```

### System keyring (requires keyring feature)

```toml
backend.auth.keyring = "imap-example"
```

Then run `himalaya account configure <account>` to store the password.
Then run `himalaya` to set up an account (the wizard prints a ready-to-save TOML config).

## Gmail Configuration

Expand All @@ -67,31 +55,24 @@ email = "you@gmail.com"
display-name = "Your Name"
default = true

backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "you@gmail.com"
backend.auth.type = "password"
backend.auth.cmd = "pass show google/app-password"

message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "you@gmail.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "pass show google/app-password"
imap.server = "imap.gmail.com:993"
imap.sasl.plain.username = "you@gmail.com"
imap.sasl.plain.password.raw = "app-password"

smtp.server = "smtp.gmail.com:587"
smtp.starttls = true
smtp.sasl.plain.username = "you@gmail.com"
smtp.sasl.plain.password.raw = "app-password"

# Gmail folder mapping. Without these, save-to-Sent fails after
# SMTP delivery succeeds (Gmail's Sent folder is `[Gmail]/Sent Mail`,
# not `Sent`), and `himalaya message send` exits non-zero. Any
# caller that retries on that error will re-run SMTP — duplicate
# emails to recipients. Always include this block for Gmail.
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/Sent Mail"
folder.aliases.drafts = "[Gmail]/Drafts"
folder.aliases.trash = "[Gmail]/Trash"
mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "[Gmail]/Sent Mail"
mailbox.alias.drafts = "[Gmail]/Drafts"
mailbox.alias.trash = "[Gmail]/Trash"
```

**Note:** Gmail requires an App Password if 2FA is enabled.
Expand All @@ -103,62 +84,47 @@ folder.aliases.trash = "[Gmail]/Trash"
email = "you@icloud.com"
display-name = "Your Name"

backend.type = "imap"
backend.host = "imap.mail.me.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "you@icloud.com"
backend.auth.type = "password"
backend.auth.cmd = "pass show icloud/app-password"

message.send.backend.type = "smtp"
message.send.backend.host = "smtp.mail.me.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "you@icloud.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "pass show icloud/app-password"
imap.server = "imap.mail.me.com:993"
imap.sasl.plain.username = "you@icloud.com"
imap.sasl.plain.password.raw = "app-password"

smtp.server = "smtp.mail.me.com:587"
smtp.starttls = true
smtp.sasl.plain.username = "you@icloud.com"
smtp.sasl.plain.password.raw = "app-password"
```

**Note:** Generate an app-specific password at appleid.apple.com

## Folder Aliases
## Mailbox Aliases

Map himalaya's canonical folder names (`inbox`, `sent`, `drafts`,
`trash`) to whatever the server actually calls them. Use the
v1.2.0 `folder.aliases.X` syntax (plural, dotted keys, directly
under `[accounts.NAME]`):
Map himalaya's canonical mailbox names (`inbox`, `sent`, `drafts`,
`trash`) to whatever the server actually calls them:

```toml
[accounts.default]
# ... other account config ...

folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
mailbox.alias.inbox = "INBOX"
mailbox.alias.sent = "Sent"
mailbox.alias.drafts = "Drafts"
mailbox.alias.trash = "Trash"
```

The equivalent TOML sub-section form also works in v1.2.0:
The equivalent TOML sub-section form also works:

```toml
[accounts.default.folder.aliases]
[accounts.default.mailbox.aliases]
inbox = "INBOX"
sent = "Sent"
drafts = "Drafts"
trash = "Trash"
```

> **Don't use the singular `alias` form.** Pre-v1.2.0 docs showed
> `[accounts.NAME.folder.alias]` (singular). v1.2.0 silently
> ignores that sub-section — TOML parses without error, but the
> alias resolver never reads it. Every lookup then falls through
> to the canonical name. On Gmail (where `sent` is actually
> `[Gmail]/Sent Mail`) this means save-to-Sent fails *after* SMTP
> delivery succeeds, and `himalaya message send` exits non-zero.
> Any caller (agent, script, user) that retries on that error
> code will re-run the send — including SMTP — producing duplicate
> emails to recipients. Always use `folder.aliases.X` (plural).
> **Note on v2.0.0 change.** Himalaya v2.0.0 renamed `folder.aliases.*`
> to `mailbox.alias.*`. If you are upgrading from v1.x, update your
> config accordingly — the old `folder.aliases.*` keys are ignored by
> v2.0.0.
## Multiple Accounts

Expand All @@ -184,21 +150,19 @@ himalaya --account work envelope list
```toml
[accounts.local]
email = "user@example.com"

backend.type = "notmuch"
backend.db-path = "~/.mail/.notmuch"
# Config structure for notmuch differs — see himalaya docs.
```

## OAuth2 Authentication (for providers that support it)

```toml
backend.auth.type = "oauth2"
backend.auth.client-id = "your-client-id"
backend.auth.client-secret.cmd = "pass show oauth/client-secret"
backend.auth.access-token.cmd = "pass show oauth/access-token"
backend.auth.refresh-token.cmd = "pass show oauth/refresh-token"
backend.auth.auth-url = "https://provider.com/oauth/authorize"
backend.auth.token-url = "https://provider.com/oauth/token"
# IMAP SASL OAuth2
imap.sasl.oauth2.client-id = "your-client-id"
imap.sasl.oauth2.client-secret.cmd = "pass show oauth/client-secret"
imap.sasl.oauth2.access-token.cmd = "pass show oauth/access-token"
imap.sasl.oauth2.refresh-token.cmd = "pass show oauth/refresh-token"
imap.sasl.oauth2.auth-url = "https://provider.com/oauth/authorize"
imap.sasl.oauth2.token-url = "https://provider.com/oauth/token"
```

## Additional Options
Expand Down
Loading
Loading