agent-message 0.1.3 → 0.1.4

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.
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "codex-message"
3
+ version = "0.1.1"
4
+ edition = "2024"
5
+
6
+ [dependencies]
7
+ anyhow = "1.0.100"
8
+ clap = { version = "4.5.53", features = ["derive", "env"] }
9
+ futures-util = "0.3.31"
10
+ rand = "0.9.2"
11
+ reqwest = { version = "0.12.24", default-features = false, features = ["json", "rustls-tls", "stream"] }
12
+ serde = { version = "1.0.228", features = ["derive"] }
13
+ serde_json = "1.0.145"
14
+ tokio = { version = "1.48.0", features = ["full"] }
15
+ tokio-util = { version = "0.7.17", features = ["io"] }
16
+ url = { version = "2.5.7", features = ["serde"] }
17
+ uuid = { version = "1.18.1", features = ["v4"] }
@@ -0,0 +1,29 @@
1
+ # codex-message
2
+
3
+ `codex-message` is a separate Rust wrapper around `codex app-server` that uses
4
+ the `agent-message` binary as its transport layer and reuses the default
5
+ `agent-message` config/profile store.
6
+
7
+ Behavior:
8
+
9
+ 1. Starts a fresh `agent-{chatId}` account with a random numeric PIN.
10
+ 2. Sends the `--to` user a startup message with the generated credentials.
11
+ 3. Reuses one Codex app-server thread for the DM session.
12
+ 4. Polls `agent-message read <user>` for new plain-text requests and relays them into `turn/start`.
13
+ 5. For approval and input requests, sends readable `json_render` prompts back to Jay and waits for a text reply.
14
+ 6. Sends final Codex results back as `json_render` reports.
15
+
16
+ Example:
17
+
18
+ ```bash
19
+ cd codex-message
20
+ cargo run -- --to jay --model gpt-5.4
21
+ ```
22
+
23
+ Useful flags:
24
+
25
+ - `--to jay`
26
+ - `--cwd /path/to/worktree`
27
+ - `--approval-policy on-request`
28
+ - `--sandbox workspace-write`
29
+ - `--network-access`
@@ -0,0 +1,205 @@
1
+ use std::path::PathBuf;
2
+ use std::process::Stdio;
3
+ use std::time::Duration;
4
+
5
+ use anyhow::{Context, Result, anyhow, bail};
6
+ use serde_json::Value;
7
+ use tokio::process::Command;
8
+
9
+ #[derive(Debug, Clone)]
10
+ pub(crate) struct AgentMessageClient {
11
+ binary: PathBuf,
12
+ }
13
+
14
+ impl AgentMessageClient {
15
+ pub(crate) fn new(binary: PathBuf) -> Self {
16
+ Self { binary }
17
+ }
18
+
19
+ pub(crate) async fn register(&self, username: &str, pin: &str) -> Result<()> {
20
+ let output = self
21
+ .run(["register", username, pin])
22
+ .await
23
+ .context("run `agent-message register`")?;
24
+ if !output.contains(&format!("registered {username}")) {
25
+ bail!("unexpected register output: {output}");
26
+ }
27
+ Ok(())
28
+ }
29
+
30
+ pub(crate) async fn send_text_message(&self, username: &str, text: &str) -> Result<String> {
31
+ let output = self
32
+ .run(["send", username, text])
33
+ .await
34
+ .context("run `agent-message send`")?;
35
+ parse_sent_message_id(&output)
36
+ }
37
+
38
+ pub(crate) async fn send_json_render_message(
39
+ &self,
40
+ username: &str,
41
+ spec: Value,
42
+ ) -> Result<String> {
43
+ let payload = serde_json::to_string(&spec).context("encode json_render spec")?;
44
+ let output = self
45
+ .run(["send", username, &payload, "--kind", "json_render"])
46
+ .await
47
+ .context("run `agent-message send --kind json_render`")?;
48
+ parse_sent_message_id(&output)
49
+ }
50
+
51
+ pub(crate) async fn read_messages(&self, username: &str, limit: usize) -> Result<Vec<Message>> {
52
+ let limit_string = limit.to_string();
53
+ let output = self
54
+ .run(["read", username, "-n", &limit_string])
55
+ .await
56
+ .context("run `agent-message read`")?;
57
+ parse_read_output(&output)
58
+ }
59
+
60
+ async fn run<const N: usize>(&self, args: [&str; N]) -> Result<String> {
61
+ let mut command = Command::new(&self.binary);
62
+ command
63
+ .args(args)
64
+ .stdin(Stdio::null())
65
+ .stdout(Stdio::piped())
66
+ .stderr(Stdio::piped());
67
+
68
+ let child = command
69
+ .spawn()
70
+ .with_context(|| format!("spawn `{}`", self.binary.display()))?;
71
+ let output = tokio::time::timeout(Duration::from_secs(30), child.wait_with_output())
72
+ .await
73
+ .context("agent-message command timed out")?
74
+ .context("wait for agent-message command")?;
75
+
76
+ let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
77
+ let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
78
+ if !output.status.success() {
79
+ let detail = if stderr.is_empty() {
80
+ stdout.clone()
81
+ } else {
82
+ stderr.clone()
83
+ };
84
+ return Err(anyhow!("agent-message command failed: {detail}"));
85
+ }
86
+ Ok(stdout)
87
+ }
88
+ }
89
+
90
+ #[derive(Debug, Clone, PartialEq, Eq)]
91
+ pub(crate) struct Message {
92
+ pub(crate) id: String,
93
+ pub(crate) sender_username: String,
94
+ pub(crate) text: String,
95
+ }
96
+
97
+ fn parse_sent_message_id(output: &str) -> Result<String> {
98
+ let trimmed = output.trim();
99
+ let Some(rest) = trimmed.strip_prefix("sent ") else {
100
+ bail!("unexpected send output: {trimmed}");
101
+ };
102
+ let id = rest.trim();
103
+ if id.is_empty() {
104
+ bail!("send output did not contain a message id");
105
+ }
106
+ Ok(id.to_string())
107
+ }
108
+
109
+ fn parse_read_output(output: &str) -> Result<Vec<Message>> {
110
+ let mut messages = Vec::new();
111
+ let mut current: Option<Message> = None;
112
+
113
+ for raw_line in output.lines() {
114
+ let line = raw_line.trim_end();
115
+ if line.trim().is_empty() {
116
+ continue;
117
+ }
118
+
119
+ if line.starts_with('[') {
120
+ if let Some(message) = current.take() {
121
+ messages.push(message);
122
+ }
123
+ current = Some(parse_read_line(line)?);
124
+ continue;
125
+ }
126
+
127
+ let Some(message) = current.as_mut() else {
128
+ bail!("unexpected read line: {line}");
129
+ };
130
+ message.text.push('\n');
131
+ message.text.push_str(line);
132
+ }
133
+
134
+ if let Some(message) = current {
135
+ messages.push(message);
136
+ }
137
+
138
+ Ok(messages)
139
+ }
140
+
141
+ fn parse_read_line(line: &str) -> Result<Message> {
142
+ let Some(after_index) = line.split_once("] ").map(|(_, rest)| rest) else {
143
+ bail!("unexpected read line: {line}");
144
+ };
145
+ let Some((message_id, rest)) = after_index.split_once(' ') else {
146
+ bail!("unexpected read line missing message id: {line}");
147
+ };
148
+ let Some((sender, text)) = rest.split_once(": ") else {
149
+ bail!("unexpected read line missing sender/text separator: {line}");
150
+ };
151
+ if message_id.trim().is_empty() || sender.trim().is_empty() {
152
+ bail!("unexpected read line with empty fields: {line}");
153
+ }
154
+
155
+ Ok(Message {
156
+ id: message_id.trim().to_string(),
157
+ sender_username: sender.trim().to_string(),
158
+ text: text.to_string(),
159
+ })
160
+ }
161
+
162
+ #[cfg(test)]
163
+ mod tests {
164
+ use super::*;
165
+
166
+ #[test]
167
+ fn parses_read_line() {
168
+ let parsed = parse_read_line("[1] m-123 jay: hello world").expect("parse line");
169
+ assert_eq!(
170
+ parsed,
171
+ Message {
172
+ id: "m-123".to_string(),
173
+ sender_username: "jay".to_string(),
174
+ text: "hello world".to_string(),
175
+ }
176
+ );
177
+ }
178
+
179
+ #[test]
180
+ fn parses_send_output() {
181
+ assert_eq!(
182
+ parse_sent_message_id("sent m-42").expect("parse send output"),
183
+ "m-42"
184
+ );
185
+ }
186
+
187
+ #[test]
188
+ fn parses_multiline_read_output() {
189
+ let output = "\
190
+ [1] m-123 jay: first line
191
+ chat_id: abc123
192
+ pin: 654321
193
+
194
+ [2] m-124 jay: second message";
195
+
196
+ let messages = parse_read_output(output).expect("parse output");
197
+
198
+ assert_eq!(messages.len(), 2);
199
+ assert_eq!(messages[0].id, "m-123");
200
+ assert_eq!(messages[0].sender_username, "jay");
201
+ assert_eq!(messages[0].text, "first line\nchat_id: abc123\npin: 654321");
202
+ assert_eq!(messages[1].id, "m-124");
203
+ assert_eq!(messages[1].text, "second message");
204
+ }
205
+ }