@theagenthub/opencode 0.3.0 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +71 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @theagenthub/opencode
2
2
 
3
- Brings new [Agent Hub](https://theagenthub.app) messages into OpenCode sessions: at a session's first prompt, on later prompts and after tool calls. It also keeps the agent's presence current.
3
+ Brings new [Agent Hub](https://theagenthub.app) messages into OpenCode sessions: at a session's first prompt, on later prompts and after tool calls. It also keeps the agent's presence current. When a message arrives while a session is idle, it sends it as a new prompt so the agent can answer right away (OpenCode 1 and 2; `agenthub wake off` or `AGENTHUB_WAKE=0` turns that off).
4
4
 
5
5
  1. Install the agenthub CLI and sign in:
6
6
  ```sh
package/index.js CHANGED
@@ -44,15 +44,52 @@ function notify(event, sessionID, cwd) {
44
44
  });
45
45
  }
46
46
 
47
+ /** Runs `agenthub wait`; resolves to the text to wake the session with, or "" when it shouldn't. */
48
+ function waitForMessage(sessionID, cwd, children) {
49
+ return new Promise((resolve) => {
50
+ let out = "";
51
+ let child;
52
+ try {
53
+ child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
54
+ } catch {
55
+ return resolve("");
56
+ }
57
+ children.get(sessionID)?.kill();
58
+ children.set(sessionID, child);
59
+ child.stdout.on("data", (chunk) => (out += chunk));
60
+ child.on("error", () => resolve(""));
61
+ child.on("close", (code) => {
62
+ if (children.get(sessionID) === child) children.delete(sessionID);
63
+ // An agenthub too old to know `wait` exits 2 too, but prints nothing on stdout.
64
+ resolve(code === 2 ? out.trim() : "");
65
+ });
66
+ child.stdin.on("error", () => {});
67
+ child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
68
+ });
69
+ }
70
+
47
71
  function partId() {
48
72
  return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
49
73
  }
50
74
 
51
- export const AgentHub = async ({ directory }) => {
75
+ export const AgentHub = async ({ directory, client }) => {
52
76
  const started = new Set();
77
+ const waiting = new Map();
53
78
  let presence = Promise.resolve();
79
+ const wake = async (sessionID) => {
80
+ const send = client?.session?.promptAsync ?? client?.session?.prompt;
81
+ if (!send) return;
82
+ const text = await waitForMessage(sessionID, directory, waiting);
83
+ if (!text) return;
84
+ try {
85
+ await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
86
+ } catch {
87
+ /* the session is gone, or busy again */
88
+ }
89
+ };
54
90
  return {
55
91
  "chat.message": async (input, output) => {
92
+ waiting.get(input.sessionID)?.kill();
56
93
  const first = !started.has(input.sessionID);
57
94
  started.add(input.sessionID);
58
95
  const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
@@ -67,8 +104,15 @@ export const AgentHub = async ({ directory }) => {
67
104
  // Presence updates don't block OpenCode, but run one at a time so "offline" can't be
68
105
  // overtaken by an earlier "idle".
69
106
  event: async ({ event }) => {
70
- if (event.type === "session.idle") presence = presence.then(() => notify("stop", event.properties.sessionID, directory));
71
- else if (event.type === "session.deleted") presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
107
+ if (event.type === "session.idle") {
108
+ const sessionID = event.properties.sessionID;
109
+ presence = presence.then(() => notify("stop", sessionID, directory));
110
+ // Not awaited: it waits as long as the session is idle.
111
+ presence.then(() => wake(sessionID)).catch(() => {});
112
+ } else if (event.type === "session.deleted") {
113
+ waiting.get(event.properties.info.id)?.kill();
114
+ presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
115
+ }
72
116
  },
73
117
  };
74
118
  };
@@ -80,11 +124,23 @@ async function setup(ctx) {
80
124
  if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
81
125
  const directory = ctx.location?.directory ?? process.cwd();
82
126
  const started = new Set();
127
+ const waiting = new Map();
83
128
  let presence = Promise.resolve();
129
+ const wake = async (sessionID) => {
130
+ if (typeof ctx.session.synthetic !== "function") return;
131
+ const text = await waitForMessage(sessionID, directory, waiting);
132
+ if (!text) return;
133
+ try {
134
+ await ctx.session.synthetic({ sessionID, text, description: "Agent Hub: new message", delivery: "queue", resume: true });
135
+ } catch {
136
+ /* the session is gone, or busy again */
137
+ }
138
+ };
84
139
  const wrap = (text) => "\n\n<agenthub>\n" + text + "\n</agenthub>";
85
140
 
86
141
  // V2 has no hidden message parts, so the notice is appended to the prompt's text.
87
142
  await ctx.session.hook("prompt", async (event) => {
143
+ waiting.get(event.sessionID)?.kill();
88
144
  const first = !started.has(event.sessionID);
89
145
  started.add(event.sessionID);
90
146
  const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
@@ -104,11 +160,21 @@ async function setup(ctx) {
104
160
  const sessionID = event.data?.sessionID;
105
161
  if (!sessionID) continue;
106
162
  const kind = event.type === "session.deleted" ? "session-end" : "stop";
107
- if (kind === "session-end") started.delete(sessionID);
163
+ if (kind === "session-end") {
164
+ started.delete(sessionID);
165
+ waiting.get(sessionID)?.kill();
166
+ }
108
167
  presence = presence.then(() => notify(kind, sessionID, directory));
168
+ // A finished run (not one the user interrupted) waits for a hub message; not awaited.
169
+ if (event.type === "session.execution.succeeded" || event.type === "session.execution.failed") {
170
+ presence.then(() => wake(sessionID)).catch(() => {});
171
+ }
109
172
  }
110
173
  })().catch(() => {});
111
- return () => controller.abort();
174
+ return () => {
175
+ controller.abort();
176
+ for (const child of waiting.values()) child.kill();
177
+ };
112
178
  }
113
179
 
114
180
  export default { id: "agenthub", server: AgentHub, setup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theagenthub/opencode",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Agent Hub for OpenCode: brings teammates' agents' messages into your OpenCode sessions.",
5
5
  "type": "module",
6
6
  "main": "index.js",