@craftsamo/signal-tools 0.0.0 → 0.1.1
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/.agents/plugins/marketplace.json +20 -0
- package/.antigravity-plugin/plugin.json +12 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +430 -4
- package/__init__.py +36 -0
- package/dist/cli.js +28639 -0
- package/dist/mcp.js +36626 -0
- package/dist/opencode.js +26103 -0
- package/mcp.json +10 -0
- package/mcp_config.json +8 -0
- package/package.json +64 -4
- package/plugin.json +13 -0
- package/plugin.yaml +34 -0
- package/skills/.gitkeep +0 -0
- package/skills/signal-account/SKILL.md +42 -0
- package/skills/signal-actions/SKILL.md +52 -0
- package/skills/signal-chats/SKILL.md +71 -0
- package/skills/signal-contacts/SKILL.md +28 -0
- package/skills/signal-files/SKILL.md +86 -0
- package/skills/signal-sends/SKILL.md +41 -0
- package/src/hermes/signal_tools_hermes/__init__.py +5 -0
- package/src/hermes/signal_tools_hermes/approval.py +78 -0
- package/src/hermes/signal_tools_hermes/context.py +31 -0
- package/src/hermes/signal_tools_hermes/listing.py +75 -0
- package/src/hermes/signal_tools_hermes/plugin.py +36 -0
- package/src/hermes/signal_tools_hermes/project.py +22 -0
- package/src/hermes/signal_tools_hermes/runtime.py +161 -0
- package/src/hermes/signal_tools_hermes/settings.py +113 -0
- package/src/hermes/signal_tools_hermes/skills.py +95 -0
- package/src/hermes/signal_tools_hermes/tools.py +79 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "craft-agent",
|
|
3
|
+
"interface": {
|
|
4
|
+
"displayName": "Craft Agent"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "signal-tools",
|
|
9
|
+
"source": {
|
|
10
|
+
"source": "local",
|
|
11
|
+
"path": "./"
|
|
12
|
+
},
|
|
13
|
+
"policy": {
|
|
14
|
+
"installation": "AVAILABLE",
|
|
15
|
+
"authentication": "ON_INSTALL"
|
|
16
|
+
},
|
|
17
|
+
"category": "Developer Tools"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signal-tools",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Signal tools for coding agents: chats, messages, contacts, received files and sends.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "craftsamo",
|
|
7
|
+
"url": "https://github.com/craft-agent"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/craft-agent/signal-tools#readme",
|
|
10
|
+
"repository": "https://github.com/craft-agent/signal-tools",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "craft-agent",
|
|
4
|
+
"description": "Plugins by Craft Agent.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "Craft Agent",
|
|
7
|
+
"url": "https://github.com/craft-agent"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "signal-tools",
|
|
12
|
+
"source": "./",
|
|
13
|
+
"description": "Signal tools for coding agents: chats, messages, contacts, received files and sends.",
|
|
14
|
+
"category": "development"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signal-tools",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Signal tools for coding agents: chats, messages, contacts, received files and sends.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "craftsamo",
|
|
7
|
+
"url": "https://github.com/craft-agent"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/craft-agent/signal-tools#readme",
|
|
10
|
+
"repository": "https://github.com/craft-agent/signal-tools",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"signal-tools": {
|
|
14
|
+
"command": "node",
|
|
15
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/dist/mcp.js"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 craftsamo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,431 @@
|
|
|
1
|
-
#
|
|
1
|
+
# signal-tools
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Signal tools for coding agents: chats, messages, contacts, received files and sends.
|
|
4
|
+
|
|
5
|
+
| Harness | Delivery | Pinned by |
|
|
6
|
+
| -------------- | ---------------------------------------------- | ------------------------------ |
|
|
7
|
+
| OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
|
|
8
|
+
| OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
|
|
9
|
+
| Claude Code | plugin: MCP server and skills | the release tag, as the ref |
|
|
10
|
+
| Codex | plugin: MCP server and skills | the release tag, as the ref |
|
|
11
|
+
| Grok Build | plugin: MCP server and skills | the release tag, as `@ref` |
|
|
12
|
+
| Antigravity | plugin: MCP server and skills | the release tag's tree URL |
|
|
13
|
+
| Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
|
|
14
|
+
| other MCP host | MCP server from npm | the version in the package |
|
|
15
|
+
|
|
16
|
+
The tools need [Node.js](https://nodejs.org/) 24 or later.
|
|
17
|
+
[`docs/conventions.md`](docs/conventions.md) describes how the tools are named
|
|
18
|
+
and how they behave.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Install from a release, never from the default branch: a release is a tag whose
|
|
23
|
+
commit carries the built `dist/`, and the default branch does not. In the
|
|
24
|
+
commands below, `<version>` is a release's version (`1.2.3`) and `<sha>` is the
|
|
25
|
+
commit its tag names:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gh api repos/craft-agent/signal-tools/commits/v<version> --jq .sha
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### OpenCode
|
|
32
|
+
|
|
33
|
+
OpenCode V2 takes the package and its options in `opencode.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plugins": [{ "package": "@craftsamo/signal-tools@<version>", "options": { "read_only": false } }],
|
|
38
|
+
"permissions": [{ "action": "signal_*", "resource": "*", "effect": "ask" }]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
OpenCode V1 takes the package as a string, or as a pair with its options:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"plugin": [["@craftsamo/signal-tools@<version>", { "read_only": false }]],
|
|
47
|
+
"permission": { "signal_*": "ask" }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
OpenCode allows any action no rule names, so the plugin's request is granted
|
|
52
|
+
silently unless a rule says `ask`. Read tools never ask, so the `signal_*` glob
|
|
53
|
+
is safe.
|
|
54
|
+
|
|
55
|
+
V1 registers no skills. Unpack the package and copy its `skills/*` folders into
|
|
56
|
+
`~/.config/opencode/skills/`:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm pack @craftsamo/signal-tools@<version>
|
|
60
|
+
tar -xzf craftsamo-signal-tools-<version>.tgz
|
|
61
|
+
mkdir -p ~/.config/opencode/skills
|
|
62
|
+
cp -r package/skills/* ~/.config/opencode/skills/
|
|
63
|
+
rm -rf package craftsamo-signal-tools-<version>.tgz
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Claude Code
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
claude plugin marketplace add craft-agent/signal-tools#v<version>
|
|
70
|
+
claude plugin install signal-tools@craft-agent
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Codex
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
codex plugin marketplace add craft-agent/signal-tools --ref v<version>
|
|
77
|
+
codex plugin add signal-tools@craft-agent
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Codex turns `-` into `_` in server names, so it shows the tools as
|
|
81
|
+
`mcp__signal_tools__signal_*`. It starts the MCP server in the plugin's
|
|
82
|
+
install directory, which is never the caller's working tree.
|
|
83
|
+
|
|
84
|
+
### Grok Build
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
grok plugin install craft-agent/signal-tools@v<version> --trust
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Grok starts the MCP server in the session's working directory.
|
|
91
|
+
|
|
92
|
+
### Antigravity
|
|
93
|
+
|
|
94
|
+
Install from the tree of the release tag, never from the default branch, which
|
|
95
|
+
has no built `dist/`; Antigravity clones the tag into
|
|
96
|
+
`~/.gemini/config/plugins/signal-tools`.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
agy plugin install https://github.com/craft-agent/signal-tools/tree/v<version>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Hermes Agent
|
|
103
|
+
|
|
104
|
+
Hermes installs a commit, not a tag, and the plugin installs disabled:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
hermes plugins install craft-agent/signal-tools --ref <sha>
|
|
108
|
+
hermes plugins enable signal-tools
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If a Hermes gateway is already running, restart it (`hermes gateway restart`) to
|
|
112
|
+
load the plugin.
|
|
113
|
+
|
|
114
|
+
### Any other MCP host
|
|
115
|
+
|
|
116
|
+
Add the server to the host's MCP configuration. Flags go in `args`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"signal-tools": {
|
|
122
|
+
"command": "npx",
|
|
123
|
+
"args": ["-y", "@craftsamo/signal-tools@<version>", "--read-only"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Tools and skills
|
|
130
|
+
|
|
131
|
+
Each toolset comes with a skill that tells an agent how to use its tools.
|
|
132
|
+
|
|
133
|
+
### Account (`account`)
|
|
134
|
+
|
|
135
|
+
| Tool | What it does | Class |
|
|
136
|
+
| --------------------- | ----------------------------------------------------------------------------------- | ----- |
|
|
137
|
+
| `signal_account_read` | each label: linked or not, the sync service's state, the last receive and next step | read |
|
|
138
|
+
|
|
139
|
+
It reads local state only: no daemon, no network, and no number, account id
|
|
140
|
+
or key in its result. The skill is
|
|
141
|
+
[`signal-account`](skills/signal-account/SKILL.md), shown as
|
|
142
|
+
`signal-tools:signal-account` in Claude Code and `signal-tools:account` in
|
|
143
|
+
Hermes.
|
|
144
|
+
|
|
145
|
+
### Chats (`chats`)
|
|
146
|
+
|
|
147
|
+
| Tool | What it does | Class |
|
|
148
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
149
|
+
| `signal_chat_read` | by `method`: `list` finds chats, `view` reads one chat's messages, `search` finds messages by words, `around` reads the messages next to a message | read |
|
|
150
|
+
|
|
151
|
+
It reads the account's local mirror only: no daemon, no network, and no number
|
|
152
|
+
of the account's own, attachment id or key in its result. History starts when
|
|
153
|
+
the device was linked. Times are local with their offset, a chat is always its
|
|
154
|
+
id, a long result is paged with `next`, and everything it returns is data
|
|
155
|
+
written by other people. The skill is
|
|
156
|
+
[`signal-chats`](skills/signal-chats/SKILL.md), shown as
|
|
157
|
+
`signal-tools:signal-chats` in Claude Code and `signal-tools:chats` in Hermes.
|
|
158
|
+
|
|
159
|
+
### Contacts (`contacts`)
|
|
160
|
+
|
|
161
|
+
| Tool | What it does | Class |
|
|
162
|
+
| --------------------- | -------------------------------------------------------------- | ----- |
|
|
163
|
+
| `signal_contact_read` | lists local contacts or checks up to three numbers with Signal | read |
|
|
164
|
+
|
|
165
|
+
Listing needs no daemon; checking numbers needs the running sync service.
|
|
166
|
+
Neither writes the mirror. The skill is
|
|
167
|
+
[`signal-contacts`](skills/signal-contacts/SKILL.md), shown as
|
|
168
|
+
`signal-tools:signal-contacts` in Claude Code and `signal-tools:contacts` in
|
|
169
|
+
Hermes.
|
|
170
|
+
|
|
171
|
+
### Files (`files`)
|
|
172
|
+
|
|
173
|
+
| Tool | What it does | Class |
|
|
174
|
+
| ------------------ | ------------------------------------------------------------------ | ----- |
|
|
175
|
+
| `signal_file_save` | saves one message's files, and unpacks its archives when asked for | add |
|
|
176
|
+
| `signal_file_send` | sends 1 to 10 files (100 MiB together), with a caption, to a chat | add |
|
|
177
|
+
|
|
178
|
+
The toolset is off by default: it writes to disk, opens archives and sends
|
|
179
|
+
files. Turn it on with `toolsets` or `tools`. Files are sent only from the
|
|
180
|
+
working directory (not when that is the home directory or a file system root)
|
|
181
|
+
and the directories the `send_dirs` setting names; keys, secrets, programs and
|
|
182
|
+
installers are refused, archives are checked entry by entry, and a file's
|
|
183
|
+
`sha256` can be given to be sure the file sent is the one approved. Sending
|
|
184
|
+
needs the sync service running. Received files go to the `download_dir` setting, in a folder
|
|
185
|
+
per message, private to the user (mode 700 and 600) and never executable;
|
|
186
|
+
nothing is overwritten. Programs and installers are never saved, and an archive
|
|
187
|
+
(zip, tar, gzipped tar) is saved only after its contents pass inspection, then
|
|
188
|
+
listed; `unpack: true` unpacks it into `<name>.unpacked/` beside it. A
|
|
189
|
+
view-once message's files are not saved. On macOS every saved file carries the
|
|
190
|
+
quarantine flag. Saving needs no daemon. The skill is
|
|
191
|
+
[`signal-files`](skills/signal-files/SKILL.md), shown as
|
|
192
|
+
`signal-tools:signal-files` in Claude Code and `signal-tools:files` in Hermes.
|
|
193
|
+
|
|
194
|
+
### Sends (`sends`)
|
|
195
|
+
|
|
196
|
+
| Tool | What it does | Class |
|
|
197
|
+
| --------------------- | ---------------------------------------------------------------------------------- | ----- |
|
|
198
|
+
| `signal_message_send` | sends one text of at most 4,000 characters to a chat, optionally quoting a message | add |
|
|
199
|
+
|
|
200
|
+
A chat is always its id; the user's own account id is Note to Self, and a group
|
|
201
|
+
the user left is refused. A result means Signal accepted the message, with
|
|
202
|
+
`reached` and `not_reached` for a group; any other failure after the request
|
|
203
|
+
went out is `uncertain`: check the phone before sending again. The message is
|
|
204
|
+
recorded in the mirror, and a record that fails is listed under `errors`
|
|
205
|
+
without undoing the send. It needs the running sync service. The skill is
|
|
206
|
+
[`signal-sends`](skills/signal-sends/SKILL.md), shown as
|
|
207
|
+
`signal-tools:signal-sends` in Claude Code and `signal-tools:sends` in Hermes.
|
|
208
|
+
|
|
209
|
+
### Actions (`actions`)
|
|
210
|
+
|
|
211
|
+
| Tool | What it does | Class |
|
|
212
|
+
| ----------------------- | ------------------------------------------------------------------------ | -------------- |
|
|
213
|
+
| `signal_message_edit` | replaces the text of one of the user's own messages | overwrite |
|
|
214
|
+
| `signal_message_delete` | deletes one of the user's own messages for everyone; it cannot be undone | administrative |
|
|
215
|
+
| `signal_message_react` | sets, replaces or removes the user's emoji reaction on a message | overwrite |
|
|
216
|
+
|
|
217
|
+
The toolset is off by default: the actions change what others have already
|
|
218
|
+
read. Turn it on with `toolsets` or `tools`. Editing and deleting take only the
|
|
219
|
+
user's own messages, within 24 hours of sending, and a message is edited at most
|
|
220
|
+
10 times; only a text message can be edited. A reaction goes to any message, one
|
|
221
|
+
per person, so a new one replaces the user's earlier one. Outcomes follow the
|
|
222
|
+
sends': a result means Signal accepted it, with `reached` and `not_reached` for
|
|
223
|
+
a group, and any other failure after the request went out is `uncertain`: check
|
|
224
|
+
the phone before repeating it. Each is recorded in the mirror as the sync
|
|
225
|
+
service would record it from another device, and a record that fails is listed
|
|
226
|
+
under `errors`. No tool sends read receipts or typing indicators. It needs the
|
|
227
|
+
running sync service. The skill is
|
|
228
|
+
[`signal-actions`](skills/signal-actions/SKILL.md), shown as
|
|
229
|
+
`signal-tools:signal-actions` in Claude Code and `signal-tools:actions` in
|
|
230
|
+
Hermes.
|
|
231
|
+
|
|
232
|
+
## Adding a tool
|
|
233
|
+
|
|
234
|
+
1. Write the spec in `src/core/<domain>/<verb>.ts` with `defineTool`: an ID
|
|
235
|
+
`<prefix>_<domain>_<verb>`, a title and description, the Zod argument and
|
|
236
|
+
output schemas, its toolset and write class, and `execute(args, context)`.
|
|
237
|
+
A write tool also gives `targets(args, context)`: what the call writes to,
|
|
238
|
+
as a permission rule should name it.
|
|
239
|
+
2. Test it beside the spec (`<verb>.test.ts`), calling it through `invoke` as
|
|
240
|
+
the entry points do. A tool that runs a program is tested against a fake of
|
|
241
|
+
that program on `PATH`; no test reaches a real service.
|
|
242
|
+
3. List it in `src/tools.ts`. A new toolset goes into `toolsets` in
|
|
243
|
+
`src/project.ts` (and `defaultToolsets` when it is on by default).
|
|
244
|
+
4. Add or update the toolset's skill, `skills/<prefix>-<toolset>/SKILL.md`.
|
|
245
|
+
5. Run `pnpm run build && pnpm run manifests` to list the tool in
|
|
246
|
+
`plugin.yaml`, then describe it under "Tools and skills".
|
|
247
|
+
|
|
248
|
+
[`docs/conventions.md`](docs/conventions.md) has the rules each step follows.
|
|
249
|
+
|
|
250
|
+
## Reads and writes
|
|
251
|
+
|
|
252
|
+
Every read tool is named `<prefix>_<domain>_read`. Reads can run unasked;
|
|
253
|
+
writes are confirmed by the harness.
|
|
254
|
+
|
|
255
|
+
| Harness | Reads without asking |
|
|
256
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------ |
|
|
257
|
+
| OpenCode | always; the plugin asks only before a write |
|
|
258
|
+
| Claude Code | the rule `mcp__plugin_signal-tools_signal-tools__*_read` in `permissions.allow` of `~/.claude/settings.json` |
|
|
259
|
+
| Codex | always; Codex runs tools that declare `readOnlyHint` unasked, as the read tools do |
|
|
260
|
+
| Grok Build | `grok --allow 'MCPTool(signal-tools__*_read)' --allow 'MCPTool(signal_*_read)'` (both rules; see below) |
|
|
261
|
+
| Antigravity | the rule `mcp(signal-tools_signal-tools/<tool id>)` in `permissions.allow`, one for each read tool |
|
|
262
|
+
| Hermes | always; the plugin asks only before a write |
|
|
263
|
+
|
|
264
|
+
Where a write is asked about, and where it is not:
|
|
265
|
+
|
|
266
|
+
- **OpenCode** grants any action no rule names, so a write runs unasked unless
|
|
267
|
+
a rule asks for it: add the `signal_*` rule shown under [OpenCode](#opencode).
|
|
268
|
+
- **Codex** under `codex exec`, and **Grok Build** under `grok -p`, refuse a
|
|
269
|
+
write they cannot ask about, unless it is allowed beforehand.
|
|
270
|
+
- **Grok Build** calls the plugin's tools through its `use_tool` tool, and the
|
|
271
|
+
agent often names a tool without its server first. Grok checks the rules
|
|
272
|
+
against the name as given, so the server-qualified rule alone does not match
|
|
273
|
+
that first call. The second rule matches the short name; a write still
|
|
274
|
+
matches neither.
|
|
275
|
+
- **Antigravity** asks before any MCP tool no rule allows, reads included, and
|
|
276
|
+
names the plugin's server `signal-tools_signal-tools` (plugin,
|
|
277
|
+
then server). `agy -p` refuses a tool it cannot ask about.
|
|
278
|
+
- **Hermes** `hermes chat -q` refuses a write it cannot ask about; `hermes -z`
|
|
279
|
+
and `--yolo` skip approval, so writes then run unasked.
|
|
280
|
+
|
|
281
|
+
## Settings
|
|
282
|
+
|
|
283
|
+
Every entry point takes the same settings. A flag wins over its environment
|
|
284
|
+
variable, and an option or setting wins over the variable too.
|
|
285
|
+
|
|
286
|
+
| Setting | Flag | Variable | Meaning |
|
|
287
|
+
| --------------- | ----------------- | ---------------------------- | ----------------------------------------------------------------------------- |
|
|
288
|
+
| `toolsets` | `--toolsets` | `SIGNAL_TOOLS_TOOLSETS` | the toolsets that are on, or `all`; default those `src/project.ts` names |
|
|
289
|
+
| `read_only` | `--read-only` | `SIGNAL_TOOLS_READ_ONLY` | registers no tool that writes; wins over every other setting |
|
|
290
|
+
| `tools` | `--tools` | `SIGNAL_TOOLS_TOOLS` | single tools to add by ID, whatever their toolset |
|
|
291
|
+
| `exclude_tools` | `--exclude-tools` | `SIGNAL_TOOLS_EXCLUDE_TOOLS` | single tools to remove by ID; wins over `tools` and `toolsets` |
|
|
292
|
+
| `confirm` | `--confirm` | `SIGNAL_TOOLS_CONFIRM` | MCP server only: `harness` (default) or `elicitation`, as the conventions say |
|
|
293
|
+
| `state_dir` | `--state-dir` | `SIGNAL_TOOLS_STATE_DIR` | where every label's state lives; default `$XDG_STATE_HOME/signal-tools` |
|
|
294
|
+
| `download_dir` | `--download-dir` | `SIGNAL_TOOLS_DOWNLOAD_DIR` | where received files are saved; default `~/Downloads/signal-tools` |
|
|
295
|
+
| `send_dirs` | `--send-dirs` | `SIGNAL_TOOLS_SEND_DIRS` | the directories files may be sent from, besides the working directory |
|
|
296
|
+
|
|
297
|
+
Lists are comma-separated in a flag or a variable. Where each harness reads them:
|
|
298
|
+
|
|
299
|
+
- **OpenCode** takes the plugin `options` shown above, with lists as arrays,
|
|
300
|
+
`read_only` as a boolean, `state_dir` and `download_dir` as paths and
|
|
301
|
+
`send_dirs` as an array of paths, and falls back to the variables. V2 runs plugins in its background service,
|
|
302
|
+
which keeps the environment it started with: a variable set where
|
|
303
|
+
`opencode run` starts has no effect until the service is restarted in that
|
|
304
|
+
environment (`opencode service restart`). V1 runs plugins in its own
|
|
305
|
+
process.
|
|
306
|
+
- **Hermes** takes `plugins.entries.signal-tools.settings` in
|
|
307
|
+
`config.yaml`:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
plugins:
|
|
311
|
+
entries:
|
|
312
|
+
signal-tools:
|
|
313
|
+
settings:
|
|
314
|
+
read_only: false
|
|
315
|
+
exclude_tools: []
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The plugin passes its own environment to the command line it runs, so the
|
|
319
|
+
variables work too. A running gateway keeps the environment it started with;
|
|
320
|
+
restart it (`hermes gateway restart`) after changing one.
|
|
321
|
+
|
|
322
|
+
- **Claude Code, Grok Build and Antigravity** start the plugin's server without
|
|
323
|
+
flags and pass it the client's environment, so set the variables in the
|
|
324
|
+
environment the harness runs in.
|
|
325
|
+
- **Codex** passes a plugin's MCP server only a few basic variables (such as
|
|
326
|
+
`PATH` and `HOME`), so the variables have no effect there. To change
|
|
327
|
+
settings, or to give the server a variable it needs, add the server under
|
|
328
|
+
`[mcp_servers]` in Codex's `config.toml` instead of using the plugin: flags
|
|
329
|
+
go in `args`, `env_vars` names the variables to forward from Codex's
|
|
330
|
+
environment, and `env` sets fixed values.
|
|
331
|
+
|
|
332
|
+
```toml
|
|
333
|
+
[mcp_servers.signal-tools]
|
|
334
|
+
command = "npx"
|
|
335
|
+
args = ["-y", "@craftsamo/signal-tools@<version>", "--read-only"]
|
|
336
|
+
env_vars = ["SIGNAL_TOOLS_STATE_DIR"]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Accounts and the sync service
|
|
340
|
+
|
|
341
|
+
The tools use a Signal account as a linked device of the user's phone, the
|
|
342
|
+
way Signal Desktop does, through [signal-cli](https://github.com/AsamK/signal-cli),
|
|
343
|
+
which you install yourself. They never read another client's data.
|
|
344
|
+
|
|
345
|
+
- **Each linked device has a label**, 1 to 24 lowercase letters, digits and
|
|
346
|
+
`_` (`default` when linking names none). A tool's `account` argument picks
|
|
347
|
+
the label; leave it out when only one device is linked.
|
|
348
|
+
- **Each label keeps its state in one directory**, `<state_dir>/<label>/`
|
|
349
|
+
(mode 700): signal-cli's data and keys, the daemon's socket, the mirror of
|
|
350
|
+
received messages, and the copies of files being sent. `state_dir` defaults
|
|
351
|
+
to `$XDG_STATE_HOME/signal-tools`, or `~/.local/state/signal-tools`. Keep it
|
|
352
|
+
out of backups you share: the keys read and send as you.
|
|
353
|
+
- **A sync service receives for each label** and keeps its mirror, and the
|
|
354
|
+
read tools read only the mirror. History starts when the device is linked;
|
|
355
|
+
messages that arrive while the service is down wait on Signal's server, for
|
|
356
|
+
as long as Signal keeps undelivered messages.
|
|
357
|
+
- **Signal unlinks a device unused for 45 days**, and every linked device
|
|
358
|
+
when the phone has been offline for 30; a running service keeps the device
|
|
359
|
+
in use. An unlinked label is linked again from the phone.
|
|
360
|
+
- **signal-cli releases age out.** A release more than about three months old
|
|
361
|
+
may stop working with Signal's servers, so keep it updated.
|
|
362
|
+
|
|
363
|
+
### Linking a device and running the sync service
|
|
364
|
+
|
|
365
|
+
The tools are tested against signal-cli 0.14. Install it first: on macOS
|
|
366
|
+
`brew install signal-cli`; elsewhere a
|
|
367
|
+
[release](https://github.com/AsamK/signal-cli/releases) (the JVM build needs
|
|
368
|
+
Java 25 or later). macOS and Linux are supported; Windows is
|
|
369
|
+
[#11](https://github.com/craft-agent/signal-tools/issues/11).
|
|
370
|
+
|
|
371
|
+
```sh
|
|
372
|
+
# Link: on the phone, Signal > Settings > Linked devices > Link new device,
|
|
373
|
+
# then scan the code this prints.
|
|
374
|
+
signal-tools-cli link --account work
|
|
375
|
+
|
|
376
|
+
# Start receiving at logon, and restart after a failure.
|
|
377
|
+
signal-tools-cli service install --account work
|
|
378
|
+
signal-tools-cli service status --account work
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`--account` may be left out when only one label exists (`link` names a new
|
|
382
|
+
one `default`), and every command takes `--state-dir` like the tools.
|
|
383
|
+
|
|
384
|
+
| Command | What it does |
|
|
385
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
386
|
+
| `link [--account <label>] [--device-name <n>]` | links this machine as a device of the phone, showing the code to scan; `--show-uri` also prints the address the code stands for |
|
|
387
|
+
| `sync [--account <label>]` | runs the sync service in the foreground, logging to standard error, until it is stopped |
|
|
388
|
+
| `service install \| uninstall \| status` | registers the sync service as a launchd agent (macOS) or a systemd user unit (Linux), removes it, or reports it |
|
|
389
|
+
| `remove --account <label> [--yes]` | uninstalls the service and deletes the label's keys, signal-cli's data, history and service log, after the label is typed back |
|
|
390
|
+
|
|
391
|
+
- **The service records this package's paths**, so run `service install`
|
|
392
|
+
again after updating the package. On macOS its log is
|
|
393
|
+
`~/Library/Logs/io.github.craft-agent.signal-tools/<label>.log`; on Linux,
|
|
394
|
+
`journalctl --user -u <unit>` as `service status` shows, and
|
|
395
|
+
`loginctl enable-linger` keeps it running while you are logged out.
|
|
396
|
+
- **A phone takes at most five linked devices**, and each label is one of
|
|
397
|
+
them.
|
|
398
|
+
- **Removing a label does not unlink it on the phone**: a linked device cannot
|
|
399
|
+
take itself off, so remove it under Settings > Linked devices as well.
|
|
400
|
+
- **To link a label again** after Signal unlinked it, run `link` again: the
|
|
401
|
+
old keys are moved aside and the history is kept, as long as the phone holds
|
|
402
|
+
the same account. A different account gets a label of its own.
|
|
403
|
+
- `signal_account_read` tells an agent which of these steps a label needs.
|
|
404
|
+
|
|
405
|
+
## Layout
|
|
406
|
+
|
|
407
|
+
- `src/project.ts` — the package's name, tool ID prefix, setting prefix and
|
|
408
|
+
toolsets.
|
|
409
|
+
- `src/core/` — the tool contract and the tools, free of any harness and of
|
|
410
|
+
the project's names.
|
|
411
|
+
- `src/tools.ts` — the list of every tool, which each entry point registers.
|
|
412
|
+
- `src/mcp/` — the MCP server, built to `dist/mcp.js` and run by
|
|
413
|
+
`signal-tools`.
|
|
414
|
+
- `src/opencode/` — the OpenCode plugin, built to `dist/opencode.js`.
|
|
415
|
+
- `src/cli/` — the command line the Hermes plugin calls, built to `dist/cli.js`
|
|
416
|
+
and run by `signal-tools-cli`.
|
|
417
|
+
- `src/hermes/`, `plugin.yaml` and `__init__.py` — the Hermes plugin.
|
|
418
|
+
- `skills/` — one skill per toolset, `skills/<name>/SKILL.md`, which every
|
|
419
|
+
harness that takes skills loads from the package or plugin root.
|
|
420
|
+
- `plugin.json`, `mcp.json`, `.mcp.json`, `.claude-plugin/`, `.agents/plugins/`,
|
|
421
|
+
`.antigravity-plugin/` and `mcp_config.json` — the plugin manifests of the
|
|
422
|
+
other harnesses. The root `.mcp.json` is the configuration Grok Build reads
|
|
423
|
+
(with `${CLAUDE_PLUGIN_ROOT}`, the variable both Grok Build and Claude Code
|
|
424
|
+
expand); when working on this repository in Claude Code, decline the project
|
|
425
|
+
MCP server it offers from that file.
|
|
426
|
+
|
|
427
|
+
How a release is made is in [`RELEASING.md`](RELEASING.md).
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
[MIT](LICENSE)
|
package/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Loads the plugin package from ``src/hermes/signal_tools_hermes`` for Hermes, which imports this file.
|
|
2
|
+
|
|
3
|
+
The package is loaded as ``<this module's name>._impl``, so Hermes, which evicts
|
|
4
|
+
everything under a plugin's module name when it reloads it or loads it for another
|
|
5
|
+
profile, evicts the package too, and each install path gets a package of its own.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_NAME = f"{__name__}._impl"
|
|
13
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent / "src" / "hermes" / "signal_tools_hermes"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _evict() -> None:
|
|
17
|
+
"""Drop the package and its submodules from ``sys.modules``."""
|
|
18
|
+
for name in list(sys.modules):
|
|
19
|
+
if name == _NAME or name.startswith(f"{_NAME}."):
|
|
20
|
+
sys.modules.pop(name, None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_spec = importlib.util.spec_from_file_location(
|
|
24
|
+
_NAME, _PACKAGE_DIR / "__init__.py", submodule_search_locations=[str(_PACKAGE_DIR)]
|
|
25
|
+
)
|
|
26
|
+
_module = importlib.util.module_from_spec(_spec)
|
|
27
|
+
_evict()
|
|
28
|
+
sys.modules[_NAME] = _module
|
|
29
|
+
try:
|
|
30
|
+
_spec.loader.exec_module(_module)
|
|
31
|
+
except BaseException:
|
|
32
|
+
_evict()
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
register = _module.register
|
|
36
|
+
__all__ = ["register"]
|