twinny 0.0.0-dev.260525150705
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/LICENSE +21 -0
- package/README.md +264 -0
- package/README.zh-CN.md +252 -0
- package/configs/banner.png +0 -0
- package/configs/logo.png +0 -0
- package/dist/app/caffeinate.d.ts +28 -0
- package/dist/app/caffeinate.js +96 -0
- package/dist/app/caffeinate.js.map +1 -0
- package/dist/app/daemon.d.ts +1 -0
- package/dist/app/daemon.js +44 -0
- package/dist/app/daemon.js.map +1 -0
- package/dist/app/lark-assets.d.ts +25 -0
- package/dist/app/lark-assets.js +108 -0
- package/dist/app/lark-assets.js.map +1 -0
- package/dist/app/startup-probe.d.ts +17 -0
- package/dist/app/startup-probe.js +90 -0
- package/dist/app/startup-probe.js.map +1 -0
- package/dist/app/wiring.d.ts +122 -0
- package/dist/app/wiring.js +694 -0
- package/dist/app/wiring.js.map +1 -0
- package/dist/cli/commands.d.ts +1 -0
- package/dist/cli/commands.js +47 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/install-wizard.d.ts +41 -0
- package/dist/cli/install-wizard.js +629 -0
- package/dist/cli/install-wizard.js.map +1 -0
- package/dist/codex/appserver.d.ts +109 -0
- package/dist/codex/appserver.js +308 -0
- package/dist/codex/appserver.js.map +1 -0
- package/dist/codex/goal.d.ts +64 -0
- package/dist/codex/goal.js +433 -0
- package/dist/codex/goal.js.map +1 -0
- package/dist/codex/index.d.ts +6 -0
- package/dist/codex/index.js +7 -0
- package/dist/codex/index.js.map +1 -0
- package/dist/codex/protocol.d.ts +95 -0
- package/dist/codex/protocol.js +205 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/codex/thread-name.d.ts +3 -0
- package/dist/codex/thread-name.js +27 -0
- package/dist/codex/thread-name.js.map +1 -0
- package/dist/codex/thread.d.ts +76 -0
- package/dist/codex/thread.js +80 -0
- package/dist/codex/thread.js.map +1 -0
- package/dist/codex/turn.d.ts +166 -0
- package/dist/codex/turn.js +746 -0
- package/dist/codex/turn.js.map +1 -0
- package/dist/config/bootstrap.d.ts +14 -0
- package/dist/config/bootstrap.js +56 -0
- package/dist/config/bootstrap.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +49 -0
- package/dist/config/loader.js +467 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/paths.d.ts +11 -0
- package/dist/config/paths.js +43 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/secrets.d.ts +33 -0
- package/dist/config/secrets.js +85 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/conversation/manager.d.ts +701 -0
- package/dist/conversation/manager.js +7673 -0
- package/dist/conversation/manager.js.map +1 -0
- package/dist/conversation/queue.d.ts +8 -0
- package/dist/conversation/queue.js +28 -0
- package/dist/conversation/queue.js.map +1 -0
- package/dist/conversation/routing.d.ts +11 -0
- package/dist/conversation/routing.js +55 -0
- package/dist/conversation/routing.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/lark/auth.d.ts +41 -0
- package/dist/lark/auth.js +132 -0
- package/dist/lark/auth.js.map +1 -0
- package/dist/lark/browser-auth.d.ts +68 -0
- package/dist/lark/browser-auth.js +258 -0
- package/dist/lark/browser-auth.js.map +1 -0
- package/dist/lark/cards.d.ts +140 -0
- package/dist/lark/cards.js +1150 -0
- package/dist/lark/cards.js.map +1 -0
- package/dist/lark/contact.d.ts +41 -0
- package/dist/lark/contact.js +122 -0
- package/dist/lark/contact.js.map +1 -0
- package/dist/lark/events.d.ts +65 -0
- package/dist/lark/events.js +218 -0
- package/dist/lark/events.js.map +1 -0
- package/dist/lark/files.d.ts +36 -0
- package/dist/lark/files.js +191 -0
- package/dist/lark/files.js.map +1 -0
- package/dist/lark/filters.d.ts +73 -0
- package/dist/lark/filters.js +678 -0
- package/dist/lark/filters.js.map +1 -0
- package/dist/lark/index.d.ts +10 -0
- package/dist/lark/index.js +11 -0
- package/dist/lark/index.js.map +1 -0
- package/dist/lark/messages.d.ts +87 -0
- package/dist/lark/messages.js +428 -0
- package/dist/lark/messages.js.map +1 -0
- package/dist/lark/openapi.d.ts +58 -0
- package/dist/lark/openapi.js +206 -0
- package/dist/lark/openapi.js.map +1 -0
- package/dist/lark/redactor.d.ts +5 -0
- package/dist/lark/redactor.js +68 -0
- package/dist/lark/redactor.js.map +1 -0
- package/dist/lark/types.d.ts +49 -0
- package/dist/lark/types.js +18 -0
- package/dist/lark/types.js.map +1 -0
- package/dist/launchd/install.d.ts +22 -0
- package/dist/launchd/install.js +114 -0
- package/dist/launchd/install.js.map +1 -0
- package/dist/launchd/plist.d.ts +10 -0
- package/dist/launchd/plist.js +61 -0
- package/dist/launchd/plist.js.map +1 -0
- package/dist/lock/index.d.ts +20 -0
- package/dist/lock/index.js +74 -0
- package/dist/lock/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +11 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +12 -0
- package/dist/markdown.js +149 -0
- package/dist/markdown.js.map +1 -0
- package/dist/observability/health.d.ts +25 -0
- package/dist/observability/health.js +187 -0
- package/dist/observability/health.js.map +1 -0
- package/dist/observability/logs.d.ts +10 -0
- package/dist/observability/logs.js +34 -0
- package/dist/observability/logs.js.map +1 -0
- package/dist/observability/system-notifications.d.ts +25 -0
- package/dist/observability/system-notifications.js +33 -0
- package/dist/observability/system-notifications.js.map +1 -0
- package/dist/profiles/guest.d.ts +19 -0
- package/dist/profiles/guest.js +241 -0
- package/dist/profiles/guest.js.map +1 -0
- package/dist/profiles/index.d.ts +5 -0
- package/dist/profiles/index.js +14 -0
- package/dist/profiles/index.js.map +1 -0
- package/dist/profiles/owner.d.ts +1 -0
- package/dist/profiles/owner.js +6 -0
- package/dist/profiles/owner.js.map +1 -0
- package/dist/store/db.d.ts +10 -0
- package/dist/store/db.js +29 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +4 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/migrations.d.ts +13 -0
- package/dist/store/migrations.js +79 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/repositories.d.ts +227 -0
- package/dist/store/repositories.js +1384 -0
- package/dist/store/repositories.js.map +1 -0
- package/dist/telemetry/client.d.ts +60 -0
- package/dist/telemetry/client.js +204 -0
- package/dist/telemetry/client.js.map +1 -0
- package/dist/telemetry/hash.d.ts +1 -0
- package/dist/telemetry/hash.js +8 -0
- package/dist/telemetry/hash.js.map +1 -0
- package/dist/telemetry/index.d.ts +4 -0
- package/dist/telemetry/index.js +5 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/posthog.d.ts +27 -0
- package/dist/telemetry/posthog.js +45 -0
- package/dist/telemetry/posthog.js.map +1 -0
- package/dist/telemetry/reporter.d.ts +13 -0
- package/dist/telemetry/reporter.js +29 -0
- package/dist/telemetry/reporter.js.map +1 -0
- package/dist/types.d.ts +330 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/version.js.map +1 -0
- package/dist/version.json +3 -0
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/manager.d.ts +14 -0
- package/dist/workspace/manager.js +69 -0
- package/dist/workspace/manager.js.map +1 -0
- package/dist/workspace/slug.d.ts +8 -0
- package/dist/workspace/slug.js +59 -0
- package/dist/workspace/slug.js.map +1 -0
- package/migrations/0001_initial.sql +102 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hachiwii
|
|
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
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Twinny
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Bridge your Feishu to CodeX**
|
|
6
|
+
|
|
7
|
+
[简体中文](./README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- macOS for the installer-managed LaunchAgent workflow.
|
|
12
|
+
- Node.js 22 or newer.
|
|
13
|
+
- Codex CLI 0.130.0 or newer in `PATH`, or set `CODEX_BINARY`.
|
|
14
|
+
- A Feishu/Lark bot app with the permissions and event subscriptions listed below.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Run the interactive installer with `npx`:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx twinny@latest install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Useful daemon commands:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx twinny@latest doctor
|
|
28
|
+
npx twinny@latest status
|
|
29
|
+
npx twinny@latest start
|
|
30
|
+
npx twinny@latest stop
|
|
31
|
+
npx twinny@latest restart
|
|
32
|
+
npx twinny@latest uninstall
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `TWINNY_HOME=/path/to/home` with any command when you are not using the default home.
|
|
36
|
+
|
|
37
|
+
## Feishu/Lark App Configuration
|
|
38
|
+
|
|
39
|
+
You need to grant these API permissions in the Feishu/Lark developer console:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
im:message.p2p_msg:readonly
|
|
43
|
+
im:message.group_msg
|
|
44
|
+
im:message:readonly
|
|
45
|
+
im:message:send_as_bot
|
|
46
|
+
im:message:update
|
|
47
|
+
im:message:recall
|
|
48
|
+
im:message.reactions:write_only
|
|
49
|
+
im:chat:read
|
|
50
|
+
im:chat:create
|
|
51
|
+
im:chat:update
|
|
52
|
+
im:resource
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Subscribe to these events/callbacks:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
im.message.receive_v1
|
|
59
|
+
im.message.recalled_v1
|
|
60
|
+
application.bot.menu_v6
|
|
61
|
+
card.action.trigger
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use the Feishu/Lark event long connection (WebSocket) mode. Twinny does not require a public HTTP callback URL for message events.
|
|
65
|
+
|
|
66
|
+
Optional bot shortcut menu entries can use these `event_key` values:
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
| Event key | Action |
|
|
70
|
+
| --------- | ---------------------------------------------------- |
|
|
71
|
+
| `help` | Send command help. |
|
|
72
|
+
| `status` | Show the current conversation and thread status. |
|
|
73
|
+
| `queue` | Toggle queue-next-message mode. |
|
|
74
|
+
| `new` | Open a new Codex thread in the current conversation. |
|
|
75
|
+
| `stop` | Stop the active turn and clear queued work. |
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
Send normal messages to the bot to start or continue a Codex turn. In groups, the owner must activate the group before ordinary messages are routed to Codex.
|
|
81
|
+
|
|
82
|
+
### Conversation Commands
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
| Command | Usage |
|
|
86
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
87
|
+
| `/help` | Show available commands. |
|
|
88
|
+
| `/status` | Show conversation, Codex thread, model, token, and queue status. |
|
|
89
|
+
| `/new` | Stop the current task, clear queued messages, and open a new Codex thread. |
|
|
90
|
+
| `/stop [all\|<side_id>]` | Stop the active task and clear queued messages. Use `all` to stop side turns too, or a side id to stop one side turn. |
|
|
91
|
+
| `/next` | Interrupt the current task and start the next queued message. |
|
|
92
|
+
| `/steer` | Inject the next queued batch into the currently running Codex turn. |
|
|
93
|
+
| `/queue [message]` | Without a message, queue your next message. With a message, add that message to the next turn. |
|
|
94
|
+
| `/goal <objective>` | Set and run a Codex goal. A later `/goal` while the goal is active updates the objective. |
|
|
95
|
+
| `/plan [message]` | Enter plan mode. If a message is provided, process it in plan mode immediately. |
|
|
96
|
+
| `/exit` | Exit plan mode in the next queued control step. |
|
|
97
|
+
| `/side <message>` or `/btw <message>` | Start an ephemeral side conversation forked from the current Codex thread. |
|
|
98
|
+
| `/compact` | Compact the current Codex thread context in the next queued control step. |
|
|
99
|
+
| `/thread [message]` | Create a new Lark topic backed by a new Codex thread. If `message` is present, proxy it into that new topic. |
|
|
100
|
+
| `/fork [message]` | Fork the current Codex thread into a new Lark topic. If `message` is present, proxy it into that new topic. |
|
|
101
|
+
| `/model <model> <effort>` | Set the model and reasoning effort for future turns in the current thread. |
|
|
102
|
+
| `/logo` | Send the Twinny logo image. |
|
|
103
|
+
| `/twinny` or `/banner` | Send the Twinny banner card. |
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
### Group Administration
|
|
107
|
+
|
|
108
|
+
Only the configured owner can run these commands:
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
| Command | Usage |
|
|
112
|
+
| ------------------------------- | ---------------------------------------------------------------------- |
|
|
113
|
+
| `/activate <owner_at\|owner\|all_at\|all> [profile]` | Activate a group, set who can route messages to Codex, refresh the group name, and optionally bind the group to a profile. |
|
|
114
|
+
| `/deactivate` | Disable Twinny in the current group and clear pending work. |
|
|
115
|
+
| `/pair {guest_ou_id} <profile>` | Authorize a non-owner P2P user and bind that user to a profile. |
|
|
116
|
+
| `/reload [profile]` | Reload all Codex profiles, or one named profile, after editing config. |
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Response modes:
|
|
120
|
+
|
|
121
|
+
- `owner_at`: only owner messages that mention the bot.
|
|
122
|
+
- `owner`: all owner messages.
|
|
123
|
+
- `all_at`: messages from any group member that mention the bot.
|
|
124
|
+
- `all`: all messages from any group member.
|
|
125
|
+
|
|
126
|
+
## Recommended Practice
|
|
127
|
+
|
|
128
|
+
Create a dedicated Feishu/Lark group for a project. Write an [AGENTS.md](http://AGENTS.md) inside the group's workspace. Let the owner activate the group with the least permissive useful mode, then create one topic per development task:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
/activate all host
|
|
132
|
+
/thread fix the login callback race
|
|
133
|
+
/thread add the GitHub README
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use `/fork` when a task needs an alternative direction while preserving the original Codex thread history:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
/fork try the smaller refactor path
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Keep each task's discussion inside its topic. This keeps Codex context, local workspace state, Lark discussion, and status cards separated by task.
|
|
143
|
+
|
|
144
|
+
## Security Notes
|
|
145
|
+
|
|
146
|
+
Twinny runs on the owner's local machine. Treat it as a local automation bridge, not as a hardened multi-tenant execution service.
|
|
147
|
+
|
|
148
|
+
The current default configuration is not fully ready for broad multi-user sharing. In particular, if you activate a group with the `host` profile and `all` response mode, every group member who can speak in that group can run work with the same Codex execution authority as the owner:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
/activate all host
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Be careful with `all_at` as well: every group member who can mention the bot can submit work when the group is bound to a powerful profile.
|
|
155
|
+
|
|
156
|
+
Before using Twinny in shared groups:
|
|
157
|
+
|
|
158
|
+
- Prefer a dedicated `codex_home` for guest or team profiles instead of sharing the owner's `~/.codex`.
|
|
159
|
+
- Configure Codex sandbox, filesystem, network, and approval-related settings for that profile.
|
|
160
|
+
- Add workspace-level `.codex` overrides where your Codex setup supports project-local safety policy.
|
|
161
|
+
- Keep `permissions.p2p_default_profile = "none"` unless you intentionally want unpaired P2P users to get access.
|
|
162
|
+
- Use `owner_at` or `owner` instead of `all` or `all_at` unless the group is tightly controlled.
|
|
163
|
+
|
|
164
|
+
## Advanced Configuration
|
|
165
|
+
|
|
166
|
+
Twinny reads `config.toml` from `TWINNY_HOME`.
|
|
167
|
+
|
|
168
|
+
Recognized fields:
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
| Field | Meaning and values |
|
|
172
|
+
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `[codex].binary` | Codex CLI executable path or command name. Defaults to `codex`. Use an absolute path when the LaunchAgent cannot find Codex through `PATH`. |
|
|
174
|
+
| `[lark.reaction].working` | Lark emoji type added while Twinny is working. Defaults to `Typing`. |
|
|
175
|
+
| `[lark.reaction].queued` | Lark emoji type added to queued messages. Defaults to `OneSecond`. |
|
|
176
|
+
| `[lark.redaction].email` | Redaction strategy for email addresses in outgoing Lark payloads. `mask` keeps the domain and masks the local part, for example `alice@example.com` becomes `a***e@example.com`; `whitespace` inserts spaces, for example `alice @ example.com`; `none` sends raw email addresses. Feishu may reject bot messages that contain raw email addresses or phone numbers. Defaults to `mask`. |
|
|
177
|
+
| `[lark.redaction].chinese_phone_number` | Redaction strategy for Chinese phone numbers in outgoing Lark payloads. `mask` keeps the first 3 and last 4 digits, for example `138****5678`; `whitespace` inserts spaces, for example `138 1234 5678`; `none` sends raw phone numbers. Feishu may reject bot messages that contain raw email addresses or phone numbers. Defaults to `mask`. |
|
|
178
|
+
| `[permissions].p2p_default_profile` | Profile used when an unpaired P2P user first messages Twinny. Use `none` to deny by default, or a configured profile name to auto-authorize. Defaults to `none`. |
|
|
179
|
+
| `[profiles.<name>].codex_home` | `CODEX_HOME` for that profile. Absolute paths are used as-is; relative paths are resolved under `TWINNY_HOME`. `host` defaults to `~/.codex`; other profiles inherit `host` unless set. |
|
|
180
|
+
| `[profiles.<name>].default_model` | Default model for new threads in that profile. `host` defaults to `gpt-5.5`; other profiles inherit `host` unless set. |
|
|
181
|
+
| `[profiles.<name>].default_effort` | Default reasoning effort for new threads in that profile. Common values are `minimal`, `low`, `medium`, `high`, and `xhigh`; `host` defaults to `medium`; other profiles inherit `host` unless set. |
|
|
182
|
+
| `[telemetry].enabled` | Boolean opt-out for telemetry-capable builds. Set to `false` to disable event capture. See [Telemetry](#telemetry). |
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
Telemetry data scope and opt-out settings are covered in [Telemetry](#telemetry).
|
|
186
|
+
|
|
187
|
+
Example `config.toml`:
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
[codex]
|
|
191
|
+
binary = "/opt/homebrew/bin/codex"
|
|
192
|
+
|
|
193
|
+
[lark.reaction]
|
|
194
|
+
working = "Typing"
|
|
195
|
+
queued = "OneSecond"
|
|
196
|
+
|
|
197
|
+
[lark.redaction]
|
|
198
|
+
email = "mask"
|
|
199
|
+
chinese_phone_number = "mask"
|
|
200
|
+
|
|
201
|
+
[permissions]
|
|
202
|
+
p2p_default_profile = "none"
|
|
203
|
+
|
|
204
|
+
[profiles.host]
|
|
205
|
+
codex_home = "~/.codex"
|
|
206
|
+
default_model = "gpt-5.5"
|
|
207
|
+
default_effort = "medium"
|
|
208
|
+
|
|
209
|
+
[profiles.guest]
|
|
210
|
+
codex_home = "./profiles/guest-codex"
|
|
211
|
+
default_model = "gpt-5.5"
|
|
212
|
+
default_effort = "medium"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Relative `codex_home` paths are resolved under `TWINNY_HOME`. Each profile starts its own Codex app-server process with `CODEX_HOME` set to that profile's `codex_home`.
|
|
216
|
+
|
|
217
|
+
After editing profile config, run `/reload [profile]` from Lark or restart the daemon.
|
|
218
|
+
|
|
219
|
+
## Multiple Instances With `TWINNY_HOME`
|
|
220
|
+
|
|
221
|
+
Run multiple isolated Twinny instances by giving each instance its own home:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest install
|
|
225
|
+
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest install
|
|
226
|
+
|
|
227
|
+
TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest status
|
|
228
|
+
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest logs
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Each home gets separate config and needs a separate Feishu bot app.
|
|
232
|
+
|
|
233
|
+
## Telemetry
|
|
234
|
+
|
|
235
|
+
Twinny builds with telemetry enabled may send anonymous, best-effort usage and reliability events. The data is used to monitor product quality, understand failure patterns, and support the maintainer's personal research interests around local-agent workflows.
|
|
236
|
+
|
|
237
|
+
Twinny does not collect or upload conversation content or credentials. This includes Lark message text, prompts, Codex answers, Feishu/Lark app secrets or tokens, Codex credentials or session tokens, chat names, sender names, raw Lark or Codex IDs, raw local paths, environment variable values, API keys, and other secrets. Identifiers such as install, conversation, thread, turn, sender, message, and Codex binary are salted and hashed locally before upload.
|
|
238
|
+
|
|
239
|
+
Telemetry may include:
|
|
240
|
+
|
|
241
|
+
- install and launch lifecycle status, startup duration, and LaunchAgent setup state;
|
|
242
|
+
- runtime health signals such as heartbeat, uptime, queue and active-turn counts, memory usage, and Lark/Codex readiness;
|
|
243
|
+
- message routing metadata such as conversation type, message or action type, route kind, queue depth, and resource counts;
|
|
244
|
+
- turn metadata such as status, type, model, reasoning effort, token counts, duration, generated image count, and error code/category when a turn fails;
|
|
245
|
+
- environment metadata such as Twinny, Codex, Node, OS version/platform/arch, Lark brand, and profile count.
|
|
246
|
+
|
|
247
|
+
Telemetry failures are ignored by the product path and should not affect install, launch, message handling, or Codex turns.
|
|
248
|
+
|
|
249
|
+
Disable telemetry in `config.toml`:
|
|
250
|
+
|
|
251
|
+
```toml
|
|
252
|
+
[telemetry]
|
|
253
|
+
enabled = false
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Or disable it for a process with an environment variable:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
TWINNY_TELEMETRY_ENABLED=false npx twinny@latest start
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Twinny
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**连接你的飞书与 CodeX**
|
|
6
|
+
|
|
7
|
+
[English](./README.md)
|
|
8
|
+
|
|
9
|
+
## 环境要求
|
|
10
|
+
|
|
11
|
+
- macOS,用于 installer 管理的 LaunchAgent 工作流。
|
|
12
|
+
- Node.js 22 或更新版本。
|
|
13
|
+
- `PATH` 中有 Codex CLI 0.130.0 或更新版本,或者设置 `CODEX_BINARY`。
|
|
14
|
+
- 一个已配置下方权限和事件订阅的 Feishu/Lark 机器人应用。
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
通过 `npx` 运行交互式 installer:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx twinny@latest install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
常用 daemon 命令:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx twinny@latest doctor
|
|
28
|
+
npx twinny@latest status
|
|
29
|
+
npx twinny@latest start
|
|
30
|
+
npx twinny@latest stop
|
|
31
|
+
npx twinny@latest restart
|
|
32
|
+
npx twinny@latest uninstall
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
如果不使用默认 home,给任意命令加上 `TWINNY_HOME=/path/to/home`。
|
|
36
|
+
|
|
37
|
+
## 飞书/Lark 应用配置
|
|
38
|
+
|
|
39
|
+
你需要在飞书/Lark 开发者后台申请这些 API 权限:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
im:message.p2p_msg:readonly
|
|
43
|
+
im:message.group_msg
|
|
44
|
+
im:message:readonly
|
|
45
|
+
im:message:send_as_bot
|
|
46
|
+
im:message:update
|
|
47
|
+
im:message:recall
|
|
48
|
+
im:message.reactions:write_only
|
|
49
|
+
im:chat:read
|
|
50
|
+
im:chat:create
|
|
51
|
+
im:chat:update
|
|
52
|
+
im:resource
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
订阅这些事件/回调:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
im.message.receive_v1
|
|
59
|
+
im.message.recalled_v1
|
|
60
|
+
application.bot.menu_v6
|
|
61
|
+
card.action.trigger
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
事件订阅方式请选择飞书/Lark 事件长连接(WebSocket)。Twinny 不需要为消息事件暴露公网 HTTP callback URL。
|
|
65
|
+
|
|
66
|
+
可选机器人快捷菜单可以配置这些 `event_key`:
|
|
67
|
+
|
|
68
|
+
| Event key | 动作 |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `help` | 发送指令帮助。 |
|
|
71
|
+
| `status` | 查看当前会话和 thread 状态。 |
|
|
72
|
+
| `queue` | 切换下一条消息排队模式。 |
|
|
73
|
+
| `new` | 在当前会话中新开 Codex thread。 |
|
|
74
|
+
| `stop` | 停止当前 turn 并清空队列。 |
|
|
75
|
+
|
|
76
|
+
## 用法
|
|
77
|
+
|
|
78
|
+
向机器人发送普通消息即可开始或继续 Codex turn。在群聊中,owner 必须先激活群聊,普通消息才会被路由给 Codex。
|
|
79
|
+
|
|
80
|
+
### 会话指令
|
|
81
|
+
|
|
82
|
+
| 指令 | 用法 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `/help` | 查看可用指令。 |
|
|
85
|
+
| `/status` | 查看会话、Codex thread、模型、token 和队列状态。 |
|
|
86
|
+
| `/new` | 停止当前任务、清空队列,并新开 Codex thread。 |
|
|
87
|
+
| `/stop [all\|<side_id>]` | 停止当前任务并清空队列。用 `all` 同时停止 side turn,或传 side id 停止指定 side turn。 |
|
|
88
|
+
| `/next` | 打断当前任务,并开始执行下一条排队消息。 |
|
|
89
|
+
| `/steer` | 把队列中的下一批消息注入当前正在运行的 Codex turn。 |
|
|
90
|
+
| `/queue [message]` | 不带 message 时让你的下一条消息排队;带 message 时把该消息加入下一轮。 |
|
|
91
|
+
| `/goal <objective>` | 设置并运行 Codex goal。goal 运行中再次发送 `/goal` 会更新目标。 |
|
|
92
|
+
| `/plan [message]` | 进入 plan mode。带 message 时直接用 plan mode 处理该消息。 |
|
|
93
|
+
| `/exit` | 在下一轮队列控制步骤中退出 plan mode。 |
|
|
94
|
+
| `/side <message>` 或 `/btw <message>` | 基于当前 Codex thread 发起临时 side conversation。 |
|
|
95
|
+
| `/compact` | 在下一轮队列控制步骤中压缩当前 Codex thread 上下文。 |
|
|
96
|
+
| `/thread [message]` | 创建一个新的 Lark 话题,并绑定新的 Codex thread。带 `message` 时会把消息代理到新话题内。 |
|
|
97
|
+
| `/fork [message]` | 从当前 Codex thread fork 出一个新的 Lark 话题。带 `message` 时会把消息代理到新话题内。 |
|
|
98
|
+
| `/model <model> <effort>` | 设置当前 thread 后续 turn 使用的模型和推理强度。 |
|
|
99
|
+
| `/logo` | 发送 Twinny logo 图片。 |
|
|
100
|
+
| `/twinny` 或 `/banner` | 发送 Twinny banner 卡片。 |
|
|
101
|
+
|
|
102
|
+
### 群管理指令
|
|
103
|
+
|
|
104
|
+
只有配置中的 owner 可以执行这些指令:
|
|
105
|
+
|
|
106
|
+
| 指令 | 用法 |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `/activate <owner_at\|owner\|all_at\|all> [profile]` | 激活群聊,设置谁可以把消息路由给 Codex,刷新群名,并可选绑定 profile。 |
|
|
109
|
+
| `/deactivate` | 停用当前群聊并清空待处理任务。 |
|
|
110
|
+
| `/pair {guest_ou_id} <profile>` | 授权非 owner 的 P2P 用户,并绑定到某个 profile。 |
|
|
111
|
+
| `/reload [profile]` | 修改配置后重载所有 Codex profiles,或只重载指定 profile。 |
|
|
112
|
+
|
|
113
|
+
响应模式:
|
|
114
|
+
|
|
115
|
+
- `owner_at`:只响应 owner 且提及 bot 的消息。
|
|
116
|
+
- `owner`:响应 owner 的所有消息。
|
|
117
|
+
- `all_at`:响应任意群成员且提及 bot 的消息。
|
|
118
|
+
- `all`:响应任意群成员的所有消息。
|
|
119
|
+
|
|
120
|
+
## 推荐实践
|
|
121
|
+
|
|
122
|
+
为项目创建一个专用飞书/Lark 群。在该群的 workspace 中写一份 [AGENTS.md](http://AGENTS.md)。由 owner 用尽量小的可用权限激活群聊,然后为每个开发任务创建一个独立话题:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
/activate all host
|
|
126
|
+
/thread fix the login callback race
|
|
127
|
+
/thread add the GitHub README
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
当一个任务需要尝试替代方向,同时保留原 Codex thread 历史时,用 `/fork`:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
/fork try the smaller refactor path
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
把每个任务的讨论留在对应话题里。这样 Codex 上下文、本地 workspace 状态、Lark 讨论和状态卡都会按任务隔离。
|
|
137
|
+
|
|
138
|
+
## 安全说明
|
|
139
|
+
|
|
140
|
+
Twinny 运行在 owner 的本地机器上。请把它当作本地自动化桥接工具,而不是已经加固过的多人共享执行服务。
|
|
141
|
+
|
|
142
|
+
当前默认配置还没有 fully ready for 广泛多人共享。尤其是当你用 `host` profile 和 `all` 响应模式激活群聊时,群里所有能发言的成员都可以用 owner 的 Codex 执行权限来运行任务:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
/activate all host
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
也要谨慎使用 `all_at`:如果群聊绑定到高权限 profile,所有能提及 bot 的成员都可以提交任务。
|
|
149
|
+
|
|
150
|
+
在共享群聊中使用 Twinny 前:
|
|
151
|
+
|
|
152
|
+
- 优先给 guest 或团队 profile 配置独立的 `codex_home`,不要共享 owner 的 `~/.codex`。
|
|
153
|
+
- 为该 profile 配置 Codex sandbox、filesystem、network 和 approval 相关安全设置。
|
|
154
|
+
- 如果你的 Codex 设置支持项目级安全策略,在 workspace 的 `.codex` 中加入 override。
|
|
155
|
+
- 除非你明确希望未配对 P2P 用户获得访问权限,否则保持 `permissions.p2p_default_profile = "none"`。
|
|
156
|
+
- 除非群成员范围非常可控,否则优先使用 `owner_at` 或 `owner`,不要直接使用 `all` 或 `all_at`。
|
|
157
|
+
|
|
158
|
+
## 进阶配置
|
|
159
|
+
|
|
160
|
+
Twinny 从 `TWINNY_HOME` 读取 `config.toml`。
|
|
161
|
+
|
|
162
|
+
支持的字段:
|
|
163
|
+
|
|
164
|
+
| 字段 | 含义和取值 |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `[codex].binary` | Codex CLI 可执行文件路径或命令名。默认是 `codex`。如果 LaunchAgent 不能通过 `PATH` 找到 Codex,建议使用绝对路径。 |
|
|
167
|
+
| `[lark.reaction].working` | Twinny 工作中给 Lark 消息添加的 emoji type。默认是 `Typing`。 |
|
|
168
|
+
| `[lark.reaction].queued` | 消息进入队列时添加的 Lark emoji type。默认是 `OneSecond`。 |
|
|
169
|
+
| `[lark.redaction].email` | 发往 Lark 的 payload 中邮箱地址的脱敏策略。`mask` 保留域名并遮蔽邮箱用户名,例如 `alice@example.com` 会变成 `a***e@example.com`;`whitespace` 插入空格,例如 `alice @ example.com`;`none` 发送明文邮箱。飞书可能会拦截包含明文邮箱或手机号的 bot message。默认是 `mask`。 |
|
|
170
|
+
| `[lark.redaction].chinese_phone_number` | 发往 Lark 的 payload 中中国手机号的脱敏策略。`mask` 保留前 3 位和后 4 位,例如 `138****5678`;`whitespace` 插入空格,例如 `138 1234 5678`;`none` 发送明文手机号。飞书可能会拦截包含明文邮箱或手机号的 bot message。默认是 `mask`。 |
|
|
171
|
+
| `[permissions].p2p_default_profile` | 未配对 P2P 用户第一次给 Twinny 发消息时使用的 profile。用 `none` 表示默认拒绝,也可以填已配置的 profile 名称来自动授权。默认是 `none`。 |
|
|
172
|
+
| `[profiles.<name>].codex_home` | 该 profile 使用的 `CODEX_HOME`。绝对路径会直接使用;相对路径会以 `TWINNY_HOME` 为基准解析。`host` 默认是 `~/.codex`;其他 profile 未设置时继承 `host`。 |
|
|
173
|
+
| `[profiles.<name>].default_model` | 该 profile 新 thread 的默认模型。`host` 默认是 `gpt-5.5`;其他 profile 未设置时继承 `host`。 |
|
|
174
|
+
| `[profiles.<name>].default_effort` | 该 profile 新 thread 的默认推理强度。常见取值是 `minimal`、`low`、`medium`、`high` 和 `xhigh`;`host` 默认是 `medium`;其他 profile 未设置时继承 `host`。 |
|
|
175
|
+
| `[telemetry].enabled` | 支持 telemetry 的构建使用的布尔关闭开关。设置为 `false` 会关闭事件采集。详见 [Telemetry](#telemetry)。 |
|
|
176
|
+
|
|
177
|
+
Telemetry 的数据范围和关闭方式见文末的 [Telemetry](#telemetry)。
|
|
178
|
+
|
|
179
|
+
示例 `config.toml`:
|
|
180
|
+
|
|
181
|
+
```toml
|
|
182
|
+
[codex]
|
|
183
|
+
binary = "/opt/homebrew/bin/codex"
|
|
184
|
+
|
|
185
|
+
[lark.reaction]
|
|
186
|
+
working = "Typing"
|
|
187
|
+
queued = "OneSecond"
|
|
188
|
+
|
|
189
|
+
[lark.redaction]
|
|
190
|
+
email = "mask"
|
|
191
|
+
chinese_phone_number = "mask"
|
|
192
|
+
|
|
193
|
+
[permissions]
|
|
194
|
+
p2p_default_profile = "none"
|
|
195
|
+
|
|
196
|
+
[profiles.host]
|
|
197
|
+
codex_home = "~/.codex"
|
|
198
|
+
default_model = "gpt-5.5"
|
|
199
|
+
default_effort = "medium"
|
|
200
|
+
|
|
201
|
+
[profiles.guest]
|
|
202
|
+
codex_home = "./profiles/guest-codex"
|
|
203
|
+
default_model = "gpt-5.5"
|
|
204
|
+
default_effort = "medium"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
相对路径形式的 `codex_home` 会以 `TWINNY_HOME` 为基准解析。每个 profile 会启动独立的 Codex app-server 进程,并把 `CODEX_HOME` 设置为该 profile 的 `codex_home`。
|
|
208
|
+
|
|
209
|
+
修改 profile 配置后,可以在 Lark 中发送 `/reload [profile]`,或者重启 daemon。
|
|
210
|
+
|
|
211
|
+
## 通过 `TWINNY_HOME` 多实例部署
|
|
212
|
+
|
|
213
|
+
给每个实例指定独立 home,就可以运行多个隔离的 Twinny 实例:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest install
|
|
217
|
+
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest install
|
|
218
|
+
|
|
219
|
+
TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest status
|
|
220
|
+
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest logs
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
每个 home 都有独立的 config,并且需要一个独立的飞书机器人应用。
|
|
224
|
+
|
|
225
|
+
## Telemetry
|
|
226
|
+
|
|
227
|
+
启用 telemetry 的 Twinny 构建可能会发送匿名、best-effort 的使用和可靠性事件。这些数据用于监测产品质量、理解失败模式,并支持维护者围绕本地 agent 工作流的个人研究兴趣。
|
|
228
|
+
|
|
229
|
+
Twinny 不会收集或上传对话内容和凭据。这包括 Lark 消息正文、prompt、Codex 回答、飞书/Lark app secret 或 token、Codex 凭据或 session token、群名、发送者名称、原始 Lark 或 Codex ID、本地路径、环境变量值、API key 以及其他 secret。install、conversation、thread、turn、sender、message、Codex binary 等标识会先在本地加 salt 并 hash,再上传 hash 后的值。
|
|
230
|
+
|
|
231
|
+
Telemetry 可能包含:
|
|
232
|
+
|
|
233
|
+
- 安装和启动生命周期状态、启动耗时、LaunchAgent 配置状态;
|
|
234
|
+
- 运行时健康信号,例如 heartbeat、uptime、队列和 active turn 数量、内存使用量、Lark/Codex ready 状态;
|
|
235
|
+
- 消息路由元数据,例如 conversation type、message 或 action type、route kind、queue depth、resource count;
|
|
236
|
+
- turn 元数据,例如状态、类型、模型、reasoning effort、token 数、耗时、生成图片数量,以及失败时的 error code/category;
|
|
237
|
+
- 环境元数据,例如 Twinny、Codex、Node、OS version/platform/arch、Lark brand、profile count。
|
|
238
|
+
|
|
239
|
+
Telemetry 上报失败不会影响主流程,不应阻断安装、启动、消息处理或 Codex turn。
|
|
240
|
+
|
|
241
|
+
可以在 `config.toml` 中关闭 telemetry:
|
|
242
|
+
|
|
243
|
+
```toml
|
|
244
|
+
[telemetry]
|
|
245
|
+
enabled = false
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
也可以用环境变量关闭当前进程:
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
TWINNY_TELEMETRY_ENABLED=false npx twinny@latest start
|
|
252
|
+
```
|
|
Binary file
|
package/configs/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import type { Logger } from "pino";
|
|
3
|
+
export interface IdleSleepPreventer {
|
|
4
|
+
start(): void;
|
|
5
|
+
stop(signal?: NodeJS.Signals): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface MacIdleSleepPreventerOptions {
|
|
8
|
+
logger?: Pick<Logger, "info" | "warn">;
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
command?: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
stopTimeoutMs?: number;
|
|
13
|
+
spawnProcess?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_CAFFEINATE_COMMAND = "/usr/bin/caffeinate";
|
|
16
|
+
export declare class MacIdleSleepPreventer implements IdleSleepPreventer {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly platform;
|
|
19
|
+
private readonly command;
|
|
20
|
+
private readonly args;
|
|
21
|
+
private readonly stopTimeoutMs;
|
|
22
|
+
private readonly spawnProcess;
|
|
23
|
+
private child?;
|
|
24
|
+
private stopping;
|
|
25
|
+
constructor(options?: MacIdleSleepPreventerOptions);
|
|
26
|
+
start(): void;
|
|
27
|
+
stop(signal?: NodeJS.Signals): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const DEFAULT_CAFFEINATE_COMMAND = "/usr/bin/caffeinate";
|
|
3
|
+
const defaultCaffeinateArgs = ["-i"];
|
|
4
|
+
export class MacIdleSleepPreventer {
|
|
5
|
+
options;
|
|
6
|
+
platform;
|
|
7
|
+
command;
|
|
8
|
+
args;
|
|
9
|
+
stopTimeoutMs;
|
|
10
|
+
spawnProcess;
|
|
11
|
+
child;
|
|
12
|
+
stopping = false;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.platform = options.platform ?? process.platform;
|
|
16
|
+
this.command = options.command ?? DEFAULT_CAFFEINATE_COMMAND;
|
|
17
|
+
this.args = options.args ?? defaultCaffeinateArgs;
|
|
18
|
+
this.stopTimeoutMs = options.stopTimeoutMs ?? 1_000;
|
|
19
|
+
this.spawnProcess = options.spawnProcess ?? spawn;
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (this.platform !== "darwin" || this.child) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.stopping = false;
|
|
26
|
+
let child;
|
|
27
|
+
try {
|
|
28
|
+
child = this.spawnProcess(this.command, this.args, {
|
|
29
|
+
stdio: "ignore"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.options.logger?.warn({ error, command: this.command, args: this.args }, "failed to start caffeinate");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.child = child;
|
|
37
|
+
child.once("error", (error) => {
|
|
38
|
+
if (this.child === child) {
|
|
39
|
+
this.child = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!this.stopping) {
|
|
42
|
+
this.options.logger?.warn({ error, command: this.command, args: this.args }, "failed to start caffeinate");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
child.once("exit", (code, signal) => {
|
|
46
|
+
if (this.child === child) {
|
|
47
|
+
this.child = undefined;
|
|
48
|
+
}
|
|
49
|
+
if (!this.stopping) {
|
|
50
|
+
this.options.logger?.warn({ code, signal }, "caffeinate exited before twinny daemon shutdown");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
this.options.logger?.info({ command: this.command, args: this.args }, "started caffeinate idle sleep assertion");
|
|
54
|
+
}
|
|
55
|
+
async stop(signal = "SIGTERM") {
|
|
56
|
+
this.stopping = true;
|
|
57
|
+
const child = this.child;
|
|
58
|
+
this.child = undefined;
|
|
59
|
+
if (!child) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!hasExited(child) && !child.killed) {
|
|
63
|
+
child.kill(signal);
|
|
64
|
+
}
|
|
65
|
+
const stopped = await waitForChildExit(child, this.stopTimeoutMs);
|
|
66
|
+
if (!stopped && !hasExited(child)) {
|
|
67
|
+
child.kill("SIGKILL");
|
|
68
|
+
await waitForChildExit(child, 1_000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function hasExited(child) {
|
|
73
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
74
|
+
}
|
|
75
|
+
function waitForChildExit(child, timeoutMs) {
|
|
76
|
+
if (hasExited(child)) {
|
|
77
|
+
return Promise.resolve(true);
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
cleanup();
|
|
82
|
+
resolve(false);
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
timeout.unref?.();
|
|
85
|
+
const onExit = () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve(true);
|
|
88
|
+
};
|
|
89
|
+
const cleanup = () => {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
child.off("exit", onExit);
|
|
92
|
+
};
|
|
93
|
+
child.once("exit", onExit);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=caffeinate.js.map
|