@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.
- package/README.md +1 -1
- package/index.js +71 -5
- 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")
|
|
71
|
-
|
|
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")
|
|
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 () =>
|
|
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 };
|