pi-onlyne 0.3.3 → 0.4.0
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 +14 -8
- package/SPEC.md +3 -2
- package/dist/index.js +138 -58
- package/dist/onlyne.d.ts +6 -5
- package/dist/onlyne.js +52 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,23 +46,21 @@ For a one-off run without installing:
|
|
|
46
46
|
pi -e npm:pi-onlyne
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
You also need an initialized Onlyne workspace
|
|
49
|
+
You also need an initialized Onlyne workspace:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
onlyne init
|
|
53
|
-
|
|
54
|
-
# Optional, in another shell: refresh the workspace-local agent skill
|
|
53
|
+
# Optional: refresh the workspace-local agent skill
|
|
55
54
|
onlyne export-skill
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
`pi-onlyne`
|
|
57
|
+
`pi-onlyne` can manage a workspace-local daemon for the current Pi session. Prefer `/onlyne daemon start|stop|restart` or `/onlyne watch on` over shelling out `onlyne run` manually. Do not combine plugin-managed daemons with ad-hoc `nohup onlyne run`, `pkill -f 'onlyne run'`, or global launchd/systemd jobs for the same workspace.
|
|
59
58
|
|
|
60
59
|
## Typical workflow
|
|
61
60
|
|
|
62
61
|
1. Initialize/configure Onlyne in your project.
|
|
63
|
-
2.
|
|
64
|
-
3.
|
|
65
|
-
4. Start watching from pi:
|
|
62
|
+
2. Install this Pi extension.
|
|
63
|
+
3. Start the workspace daemon and watch from pi:
|
|
66
64
|
|
|
67
65
|
```text
|
|
68
66
|
/onlyne watch on
|
|
@@ -74,14 +72,22 @@ When a normal user message arrives through Onlyne, pi receives it as a follow-up
|
|
|
74
72
|
|
|
75
73
|
```text
|
|
76
74
|
/onlyne status
|
|
75
|
+
/onlyne daemon start
|
|
76
|
+
/onlyne daemon stop
|
|
77
|
+
/onlyne daemon restart
|
|
77
78
|
/onlyne watch on
|
|
78
79
|
/onlyne watch off
|
|
79
80
|
/onlyne config auto-start
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
`/onlyne` supports argument completions for `status`, `daemon start|stop|restart`, `watch on`, `watch off`, and `config auto-start`.
|
|
84
|
+
|
|
82
85
|
## Agent tools
|
|
83
86
|
|
|
84
87
|
```text
|
|
88
|
+
onlyne_daemon_start()
|
|
89
|
+
onlyne_daemon_stop()
|
|
90
|
+
onlyne_daemon_restart()
|
|
85
91
|
onlyne_reply({ text })
|
|
86
92
|
onlyne_send({ channelId, text, rawText? })
|
|
87
93
|
onlyne_broadcast({ targets, text, rawText? })
|
|
@@ -116,7 +122,7 @@ onlyne_broadcast({
|
|
|
116
122
|
{ channelId: "telegram" },
|
|
117
123
|
{ channelId: "feishu" }
|
|
118
124
|
],
|
|
119
|
-
text: "# Release shipped\n\nVersion 0.3.
|
|
125
|
+
text: "# Release shipped\n\nVersion 0.3.4 is live."
|
|
120
126
|
})
|
|
121
127
|
```
|
|
122
128
|
|
package/SPEC.md
CHANGED
|
@@ -7,8 +7,9 @@ Pi extension for Onlyne. Onlyne remains a workspace-local IM broker; this extens
|
|
|
7
7
|
## v1 Decisions
|
|
8
8
|
|
|
9
9
|
- Watch is configurable; default manual.
|
|
10
|
-
- `
|
|
11
|
-
-
|
|
10
|
+
- `/onlyne` provides argument completions for its supported subcommands, including daemon lifecycle commands.
|
|
11
|
+
- `watch on` connects to the workspace-local `.onlyne/run/onlyne.sock`; if unavailable, it starts a Pi-owned workspace daemon.
|
|
12
|
+
- `/onlyne daemon start|stop|restart` is the preferred lifecycle surface. Agents must not use ad-hoc `nohup onlyne run`, `pkill -f 'onlyne run'`, or global launchd/systemd jobs when pi-onlyne owns the daemon.
|
|
12
13
|
- Inbound events come from Onlyne `subscribe_events`; no polling.
|
|
13
14
|
- Inbound mode is rule-based: `auto-handle`, `queue-only`, or `muted`.
|
|
14
15
|
- Outbound defaults to `guarded-explicit`: prefer tool reply, fallback to final text, else send configured error text.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { broadcast, connectDaemon, loopback, markConsumed, sendWithRetry, stopProcess, subscribe } from "./onlyne.js";
|
|
3
|
+
import { broadcast, connectDaemon, loopback, markConsumed, sendWithRetry, shutdownDaemon, stopProcess, subscribe } from "./onlyne.js";
|
|
4
4
|
import { inboundModeFor, loadConfig, saveConfig } from "./config.js";
|
|
5
5
|
import { findWorkspace } from "./workspace.js";
|
|
6
6
|
const state = { cwd: process.cwd(), workspace: null, watching: false, owner: "stopped" };
|
|
@@ -9,29 +9,97 @@ const currentConfig = () => loadConfig(state.cwd);
|
|
|
9
9
|
function inboundText(data) { const msg = data?.data?.data ?? data?.data ?? data; const channelId = msg.channel_id ?? msg.channelId; const conversationId = msg.conversation_id ?? msg.conversationId; const messageId = msg.message_id ?? msg.messageId; const text = msg.text ?? msg.content ?? msg.body; return channelId && conversationId && typeof text === "string" ? { channelId, conversationId, messageId, text } : null; }
|
|
10
10
|
function consumeIfNotified(inbound) { if (state.workspace && inbound.messageId)
|
|
11
11
|
void markConsumed(state.workspace.socketPath, inbound.messageId).catch(() => { }); }
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
function clearReminder() { if (state.reminderTimer)
|
|
13
|
+
clearTimeout(state.reminderTimer); state.reminderTimer = undefined; }
|
|
14
|
+
function needsReply(inbound = state.currentInbound) { return !!inbound && !inbound.replied && !inbound.noReply && !!state.workspace; }
|
|
15
|
+
function scheduleReminder(pi, delayMs = 30_000) {
|
|
16
|
+
clearReminder();
|
|
17
|
+
const inbound = state.currentInbound;
|
|
18
|
+
if (!inbound || !needsReply(inbound))
|
|
19
|
+
return;
|
|
20
|
+
state.reminderTimer = setTimeout(() => {
|
|
21
|
+
state.reminderTimer = undefined;
|
|
22
|
+
if (!needsReply(inbound) || state.currentInbound !== inbound)
|
|
23
|
+
return;
|
|
24
|
+
const cfg = currentConfig();
|
|
25
|
+
if (cfg.outbound.defaultReplyMode === "explicit-only")
|
|
26
|
+
return;
|
|
27
|
+
if (cfg.outbound.defaultReplyMode === "guarded-explicit" && inbound.reminders < cfg.outbound.guardedExplicit.reminders) {
|
|
28
|
+
if (inbound.reminders === 0)
|
|
29
|
+
inbound.fallbackText = state.lastValidOutput;
|
|
30
|
+
inbound.reminders++;
|
|
31
|
+
pi.sendUserMessage(`Onlyne reminder ${inbound.reminders}/${cfg.outbound.guardedExplicit.reminders}: reply to ${inbound.channelId}/${inbound.conversationId} with onlyne_reply, or call onlyne_mark_no_reply.`, { deliverAs: "followUp" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
void reply(inbound.fallbackText || state.lastValidOutput || cfg.outbound.guardedExplicit.noOutputFallbackText).catch(() => { });
|
|
35
|
+
}, delayMs);
|
|
36
|
+
}
|
|
37
|
+
function scheduleReconnect(pi) {
|
|
38
|
+
if (state.reconnectTimer || !state.watching || !state.workspace)
|
|
39
|
+
return;
|
|
40
|
+
state.reconnectTimer = setTimeout(async () => {
|
|
41
|
+
state.reconnectTimer = undefined;
|
|
42
|
+
if (!state.watching)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
await startWatch(pi);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
scheduleReconnect(pi);
|
|
49
|
+
}
|
|
50
|
+
}, 1000);
|
|
51
|
+
}
|
|
52
|
+
async function startWatch(pi) {
|
|
53
|
+
state.workspace = findWorkspace(state.cwd);
|
|
54
|
+
if (!state.workspace)
|
|
55
|
+
throw new Error("current workspace has no .onlyne configuration");
|
|
56
|
+
if (state.reconnectTimer)
|
|
57
|
+
clearTimeout(state.reconnectTimer);
|
|
58
|
+
state.reconnectTimer = undefined;
|
|
59
|
+
state.socket?.destroy();
|
|
60
|
+
state.socket = undefined;
|
|
61
|
+
const conn = await connectDaemon(state.workspace);
|
|
62
|
+
state.owner = conn.owner;
|
|
63
|
+
state.child = conn.process;
|
|
64
|
+
const socket = subscribe(state.workspace.socketPath, (line) => { if (!line?.event || line.type !== "inbound_message")
|
|
65
|
+
return; const inbound = inboundText(line); if (!inbound)
|
|
66
|
+
return; const mode = inboundModeFor(currentConfig(), inbound.channelId, inbound.conversationId); if (mode === "muted")
|
|
67
|
+
return; if (inbound.channelId === "loopback") {
|
|
68
|
+
if (mode === "auto-handle")
|
|
69
|
+
pi.sendUserMessage(`Onlyne loopback activation${inbound.conversationId ? ` (${inbound.conversationId})` : ""}:\n\n${inbound.text}`, { deliverAs: "followUp" });
|
|
70
|
+
consumeIfNotified(inbound);
|
|
71
|
+
return;
|
|
72
|
+
} if (inbound.text.trim() === "/handshake") {
|
|
73
|
+
consumeIfNotified(inbound);
|
|
74
|
+
return;
|
|
75
|
+
} clearReminder(); state.currentInbound = { ...inbound, replied: false, noReply: false, reminders: 0 }; if (mode === "auto-handle") {
|
|
76
|
+
pi.sendUserMessage(`Onlyne inbound message from ${inbound.channelId}/${inbound.conversationId}:\n\n${inbound.text}\n\nReply with onlyne_reply, or call onlyne_mark_no_reply if no reply is needed.`, { deliverAs: "followUp" });
|
|
77
|
+
consumeIfNotified(inbound);
|
|
78
|
+
} }, () => { if (state.socket === socket)
|
|
79
|
+
scheduleReconnect(pi); });
|
|
80
|
+
state.socket = socket;
|
|
81
|
+
state.watching = true;
|
|
82
|
+
return `watching ${state.workspace.root} (${state.owner})`;
|
|
83
|
+
}
|
|
84
|
+
function stopWatch() { if (state.reconnectTimer)
|
|
85
|
+
clearTimeout(state.reconnectTimer); state.reconnectTimer = undefined; clearReminder(); state.socket?.destroy(); state.socket = undefined; stopProcess(state.child); state.child = undefined; state.watching = false; state.owner = "stopped"; return "watch stopped"; }
|
|
86
|
+
async function startDaemon() { state.workspace = findWorkspace(state.cwd); if (!state.workspace)
|
|
87
|
+
throw new Error("current workspace has no .onlyne configuration"); const conn = await connectDaemon(state.workspace, true); state.owner = conn.owner; state.child = conn.process; return `daemon ${state.owner === "extension" ? "started" : "already running"} for ${state.workspace.root}`; }
|
|
88
|
+
async function stopDaemon() { if (!state.workspace)
|
|
89
|
+
state.workspace = findWorkspace(state.cwd); if (!state.workspace)
|
|
90
|
+
throw new Error("current workspace has no .onlyne configuration"); clearReminder(); state.socket?.destroy(); state.socket = undefined; await shutdownDaemon(state.workspace, state.child); state.child = undefined; state.watching = false; state.owner = "stopped"; return `daemon stopped for ${state.workspace.root}`; }
|
|
91
|
+
async function restartDaemon() { await stopDaemon().catch(() => { }); return startDaemon(); }
|
|
29
92
|
async function reply(text) { if (!state.workspace)
|
|
30
93
|
throw new Error("onlyne workspace not found"); const inbound = state.currentInbound; if (!inbound)
|
|
31
|
-
throw new Error("no active inbound message"); const res = await sendWithRetry(state.workspace.socketPath, { channelId: inbound.channelId }, text, currentConfig().outbound.retry.attempts); if (res.ok)
|
|
32
|
-
inbound.replied = true;
|
|
94
|
+
throw new Error("no active inbound message"); const res = await sendWithRetry(state.workspace.socketPath, { channelId: inbound.channelId }, text, currentConfig().outbound.retry.attempts); if (res.ok) {
|
|
95
|
+
inbound.replied = true;
|
|
96
|
+
clearReminder();
|
|
97
|
+
} return res; }
|
|
33
98
|
export default function onlyne(pi) {
|
|
34
|
-
pi.on("session_start", async (_event, ctx) => { const resumeWatch = state.watching;
|
|
99
|
+
pi.on("session_start", async (_event, ctx) => { const resumeWatch = state.watching; if (state.owner === "extension")
|
|
100
|
+
await stopDaemon().catch(() => { });
|
|
101
|
+
else
|
|
102
|
+
stopWatch(); state.cwd = ctx.cwd; state.workspace = findWorkspace(ctx.cwd); state.currentInbound = undefined; state.lastValidOutput = undefined; ctx.ui.setStatus("onlyne", state.workspace ? "onlyne: ready" : "onlyne: no .onlyne"); if ((currentConfig().watch.autoStart || resumeWatch) && state.workspace) {
|
|
35
103
|
try {
|
|
36
104
|
ctx.ui.notify(await startWatch(pi), "info");
|
|
37
105
|
}
|
|
@@ -39,46 +107,56 @@ export default function onlyne(pi) {
|
|
|
39
107
|
ctx.ui.notify(String(e), "warning");
|
|
40
108
|
}
|
|
41
109
|
} });
|
|
42
|
-
pi.on("session_shutdown", async () => {
|
|
110
|
+
pi.on("session_shutdown", async () => { if (state.owner === "extension")
|
|
111
|
+
await stopDaemon().catch(() => { });
|
|
112
|
+
else
|
|
113
|
+
stopWatch(); });
|
|
43
114
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
44
115
|
process.once(sig, () => stopWatch());
|
|
45
116
|
pi.on("message_end", async (event) => { const text = typeof event.content === "string" ? event.content.trim() : ""; if (text && !text.startsWith("{") && !text.startsWith("[onlyne-internal]"))
|
|
46
117
|
state.lastValidOutput = text; });
|
|
47
|
-
pi.on("
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
pi.on("agent_start", async () => clearReminder());
|
|
119
|
+
pi.on("agent_end", async () => scheduleReminder(pi));
|
|
120
|
+
pi.registerCommand("onlyne", {
|
|
121
|
+
description: "Onlyne watch/status/config commands",
|
|
122
|
+
getArgumentCompletions: (prefix) => {
|
|
123
|
+
const commands = ["status", "watch on", "watch off", "daemon start", "daemon stop", "daemon restart", "config auto-start"];
|
|
124
|
+
const p = prefix.trimStart();
|
|
125
|
+
const filtered = commands.filter((c) => c.startsWith(p));
|
|
126
|
+
return filtered.length ? filtered.map((value) => ({ value, label: value })) : null;
|
|
127
|
+
},
|
|
128
|
+
handler: async (argLine, ctx) => {
|
|
129
|
+
const [cmd, sub] = argLine.trim().split(/\s+/);
|
|
130
|
+
try {
|
|
131
|
+
if (cmd === "watch" && sub === "on")
|
|
132
|
+
ctx.ui.notify(await startWatch(pi), "info");
|
|
133
|
+
else if (cmd === "watch" && sub === "off")
|
|
134
|
+
ctx.ui.notify(stopWatch(), "info");
|
|
135
|
+
else if (cmd === "daemon" && sub === "start")
|
|
136
|
+
ctx.ui.notify(await startDaemon(), "info");
|
|
137
|
+
else if (cmd === "daemon" && sub === "stop")
|
|
138
|
+
ctx.ui.notify(await stopDaemon(), "info");
|
|
139
|
+
else if (cmd === "daemon" && sub === "restart")
|
|
140
|
+
ctx.ui.notify(await restartDaemon(), "info");
|
|
141
|
+
else if (cmd === "status")
|
|
142
|
+
ctx.ui.notify(`onlyne ${state.watching ? "watching" : "stopped"}; owner=${state.owner}; workspace=${state.workspace?.root ?? "none"}`, "info");
|
|
143
|
+
else if (cmd === "config" && sub === "auto-start") {
|
|
144
|
+
const cfg = currentConfig();
|
|
145
|
+
cfg.watch.autoStart = !cfg.watch.autoStart;
|
|
146
|
+
saveConfig(state.cwd, cfg);
|
|
147
|
+
ctx.ui.notify(`autoStart=${cfg.watch.autoStart}`, "info");
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
ctx.ui.notify("usage: /onlyne status | watch on|off | daemon start|stop|restart | config auto-start", "info");
|
|
75
151
|
}
|
|
76
|
-
|
|
77
|
-
ctx.ui.notify(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
catch (e) {
|
|
153
|
+
ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
pi.registerTool(defineTool({ name: "onlyne_daemon_start", label: "Onlyne daemon start", description: "Start or connect to the current workspace-local Onlyne daemon managed by pi-onlyne.", parameters: Type.Object({}), executionMode: "parallel", async execute() { const res = await startDaemon(); return textResult(res, { owner: state.owner, workspace: state.workspace?.root }); } }));
|
|
158
|
+
pi.registerTool(defineTool({ name: "onlyne_daemon_stop", label: "Onlyne daemon stop", description: "Stop the current workspace-local Onlyne daemon when pi-onlyne manages it, without shelling out to pkill/nohup.", parameters: Type.Object({}), executionMode: "parallel", async execute() { const res = await stopDaemon(); return textResult(res, { owner: state.owner, workspace: state.workspace?.root }); } }));
|
|
159
|
+
pi.registerTool(defineTool({ name: "onlyne_daemon_restart", label: "Onlyne daemon restart", description: "Restart the current workspace-local Onlyne daemon through pi-onlyne lifecycle management.", parameters: Type.Object({}), executionMode: "parallel", async execute() { const res = await restartDaemon(); return textResult(res, { owner: state.owner, workspace: state.workspace?.root }); } }));
|
|
82
160
|
pi.registerTool(defineTool({ name: "onlyne_reply", label: "Onlyne reply", description: "Reply with plain text to the current Onlyne inbound message.", parameters: Type.Object({ text: Type.String() }), executionMode: "parallel", async execute(_id, params) { return textResult(JSON.stringify(await reply(params.text))); } }));
|
|
83
161
|
pi.registerTool(defineTool({ name: "onlyne_send", label: "Onlyne send", description: "Send Markdown to the channel's configured Onlyne conversation. Set rawText=true only for literal plain text.", parameters: Type.Object({ channelId: Type.String(), text: Type.String(), rawText: Type.Optional(Type.Boolean()) }), executionMode: "parallel", async execute(_id, params) { if (!state.workspace)
|
|
84
162
|
throw new Error("onlyne workspace not found"); const res = await sendWithRetry(state.workspace.socketPath, params, params.text, currentConfig().outbound.retry.attempts, params.rawText ?? false); return textResult(JSON.stringify(res), res); } }));
|
|
@@ -86,6 +164,8 @@ export default function onlyne(pi) {
|
|
|
86
164
|
throw new Error("onlyne workspace not found"); const cfg = currentConfig(); const results = await broadcast(state.workspace.socketPath, params.targets, params.text, cfg.outbound.retry.attempts, cfg.outbound.retry.concurrency, params.rawText ?? false); return textResult(JSON.stringify({ ok: results.every((r) => r.ok), results }), results); } }));
|
|
87
165
|
pi.registerTool(defineTool({ name: "onlyne_loopback", label: "Onlyne loopback", description: "Inject a local loopback activation message so scripts can wake the current Pi session. Set rawText=false for Markdown. FIFO alternative: write to .onlyne/channels/loopback/in.", parameters: Type.Object({ text: Type.String(), rawText: Type.Optional(Type.Boolean()) }), executionMode: "parallel", async execute(_id, params) { if (!state.workspace)
|
|
88
166
|
throw new Error("onlyne workspace not found"); const res = await loopback(state.workspace.socketPath, params.text, params.rawText ?? true); return textResult(JSON.stringify(res), res); } }));
|
|
89
|
-
pi.registerTool(defineTool({ name: "onlyne_mark_no_reply", label: "Onlyne no reply", description: "Mark the current Onlyne inbound message as intentionally not replied.", parameters: Type.Object({ reason: Type.Optional(Type.String()) }), executionMode: "parallel", async execute(_id, params) { if (state.currentInbound)
|
|
90
|
-
state.currentInbound.noReply = true;
|
|
167
|
+
pi.registerTool(defineTool({ name: "onlyne_mark_no_reply", label: "Onlyne no reply", description: "Mark the current Onlyne inbound message as intentionally not replied.", parameters: Type.Object({ reason: Type.Optional(Type.String()) }), executionMode: "parallel", async execute(_id, params) { if (state.currentInbound) {
|
|
168
|
+
state.currentInbound.noReply = true;
|
|
169
|
+
clearReminder();
|
|
170
|
+
} return textResult("marked no reply", params); } }));
|
|
91
171
|
}
|
package/dist/onlyne.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
import { type Socket } from "node:net";
|
|
3
3
|
import type { Workspace } from "./workspace.js";
|
|
4
4
|
export interface OnlyneRequest {
|
|
@@ -19,13 +19,14 @@ export interface SendResult extends SendTarget {
|
|
|
19
19
|
error?: string;
|
|
20
20
|
}
|
|
21
21
|
export declare function request(socketPath: string, req: OnlyneRequest): Promise<any>;
|
|
22
|
-
export declare function subscribe(socketPath: string, onLine: (line: any) => void): Socket;
|
|
22
|
+
export declare function subscribe(socketPath: string, onLine: (line: any) => void, onDisconnect?: () => void): Socket;
|
|
23
23
|
export declare function waitForSocket(socketPath: string, timeoutMs?: number): Promise<void>;
|
|
24
|
-
export declare function connectDaemon(ws: Workspace): Promise<{
|
|
25
|
-
owner: "external";
|
|
24
|
+
export declare function connectDaemon(ws: Workspace, startIfMissing?: boolean): Promise<{
|
|
25
|
+
owner: "external" | "extension";
|
|
26
26
|
process?: ChildProcess;
|
|
27
27
|
}>;
|
|
28
|
-
export declare function
|
|
28
|
+
export declare function shutdownDaemon(ws: Workspace, child?: ChildProcess): Promise<void>;
|
|
29
|
+
export declare function stopProcess(child?: ChildProcess): void;
|
|
29
30
|
export declare function loopback(socketPath: string, text: string, rawText?: boolean): Promise<any>;
|
|
30
31
|
export declare function markConsumed(socketPath: string, messageId: string): Promise<any>;
|
|
31
32
|
export declare function sendWithRetry(socketPath: string, target: SendTarget, text: string, attempts: number, rawText?: boolean): Promise<SendResult>;
|
package/dist/onlyne.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { createConnection } from "node:net";
|
|
2
3
|
export function request(socketPath, req) {
|
|
3
4
|
return new Promise((resolve, reject) => {
|
|
@@ -17,11 +18,17 @@ export function request(socketPath, req) {
|
|
|
17
18
|
} });
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
|
-
export function subscribe(socketPath, onLine) {
|
|
21
|
+
export function subscribe(socketPath, onLine, onDisconnect) {
|
|
21
22
|
const socket = createConnection(socketPath);
|
|
22
23
|
let buf = "";
|
|
24
|
+
let closed = false;
|
|
23
25
|
socket.setEncoding("utf8");
|
|
24
|
-
|
|
26
|
+
const disconnect = () => { if (closed)
|
|
27
|
+
return; closed = true; onDisconnect?.(); };
|
|
28
|
+
socket.on("error", disconnect);
|
|
29
|
+
socket.on("close", disconnect);
|
|
30
|
+
socket.on("connect", () => { if (!socket.destroyed)
|
|
31
|
+
socket.write('{"id":"sub","op":"subscribe_events"}\n', () => { }); });
|
|
25
32
|
socket.on("data", (chunk) => { buf += chunk; for (;;) {
|
|
26
33
|
const idx = buf.indexOf("\n");
|
|
27
34
|
if (idx < 0)
|
|
@@ -52,16 +59,56 @@ export async function waitForSocket(socketPath, timeoutMs = 5000) {
|
|
|
52
59
|
}
|
|
53
60
|
throw last instanceof Error ? last : new Error("onlyne socket not ready");
|
|
54
61
|
}
|
|
55
|
-
|
|
62
|
+
function spawnManagedDaemon(ws) {
|
|
63
|
+
const bin = process.env.ONLYNE_BIN || "onlyne";
|
|
64
|
+
const script = `
|
|
65
|
+
parent="$1"; shift
|
|
66
|
+
"$@" &
|
|
67
|
+
child=$!
|
|
68
|
+
trap 'kill "$child" 2>/dev/null; wait "$child" 2>/dev/null' INT TERM HUP EXIT
|
|
69
|
+
while kill -0 "$parent" 2>/dev/null; do
|
|
70
|
+
kill -0 "$child" 2>/dev/null || { wait "$child"; exit $?; }
|
|
71
|
+
sleep 1
|
|
72
|
+
done
|
|
73
|
+
kill "$child" 2>/dev/null
|
|
74
|
+
wait "$child" 2>/dev/null
|
|
75
|
+
`;
|
|
76
|
+
return spawn("sh", ["-c", script, "onlyne-supervisor", String(process.pid), bin, "--workspace", ws.root, "run"], { stdio: "ignore" });
|
|
77
|
+
}
|
|
78
|
+
export async function connectDaemon(ws, startIfMissing = true) {
|
|
56
79
|
try {
|
|
57
80
|
await request(ws.socketPath, { id: "ping", op: "ping" });
|
|
58
81
|
return { owner: "external" };
|
|
59
82
|
}
|
|
60
83
|
catch (e) {
|
|
61
|
-
|
|
84
|
+
if (!startIfMissing)
|
|
85
|
+
throw new Error(`onlyne daemon is not running for ${ws.root}; start it with /onlyne daemon start`, { cause: e });
|
|
86
|
+
const child = spawnManagedDaemon(ws);
|
|
87
|
+
try {
|
|
88
|
+
await waitForSocket(ws.socketPath);
|
|
89
|
+
// 自己 spawn 的 daemon 已退出:竞态中输给了其他启动方,socket 归对方所有,降级为 external(只订阅、不 shutdown)。
|
|
90
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
91
|
+
return { owner: "external" };
|
|
92
|
+
return { owner: "extension", process: child };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
stopProcess(child);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
62
98
|
}
|
|
63
99
|
}
|
|
64
|
-
export function
|
|
100
|
+
export async function shutdownDaemon(ws, child) {
|
|
101
|
+
try {
|
|
102
|
+
await request(ws.socketPath, { id: `shutdown-${Date.now()}`, op: "shutdown" });
|
|
103
|
+
}
|
|
104
|
+
catch { /* may already be down */ }
|
|
105
|
+
stopProcess(child);
|
|
106
|
+
}
|
|
107
|
+
export function stopProcess(child) { if (!child || child.killed)
|
|
108
|
+
return; try {
|
|
109
|
+
child.kill("SIGTERM");
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ } }
|
|
65
112
|
export async function loopback(socketPath, text, rawText = true) {
|
|
66
113
|
return request(socketPath, { id: `loopback-${Date.now()}`, op: "loopback", text, raw_text: rawText });
|
|
67
114
|
}
|