pi-onlyne 0.3.0 → 0.3.3
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 +9 -11
- package/SPEC.md +3 -2
- package/dist/index.js +7 -5
- package/dist/onlyne.d.ts +4 -5
- package/dist/onlyne.js +5 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,31 +46,29 @@ For a one-off run without installing:
|
|
|
46
46
|
pi -e npm:pi-onlyne
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
You also need
|
|
49
|
+
You also need an initialized Onlyne workspace and a running workspace-local daemon:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
onlyne init
|
|
53
|
-
|
|
53
|
+
onlyne run
|
|
54
|
+
# Optional, in another shell: refresh the workspace-local agent skill
|
|
54
55
|
onlyne export-skill
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
export ONLYNE_BIN=/path/to/onlyne
|
|
61
|
-
```
|
|
58
|
+
`pi-onlyne` does not install launchd/systemd jobs and does not spawn a global daemon. If you want background supervision, wrap `onlyne --workspace /path/to/project run` yourself per workspace.
|
|
62
59
|
|
|
63
60
|
## Typical workflow
|
|
64
61
|
|
|
65
62
|
1. Initialize/configure Onlyne in your project.
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
63
|
+
2. Start that workspace's daemon with `onlyne run`.
|
|
64
|
+
3. Install this Pi extension.
|
|
65
|
+
4. Start watching from pi:
|
|
68
66
|
|
|
69
67
|
```text
|
|
70
68
|
/onlyne watch on
|
|
71
69
|
```
|
|
72
70
|
|
|
73
|
-
When a message arrives through Onlyne, pi receives it as a follow-up message. The agent can then call `onlyne_reply`, or deliberately call `onlyne_mark_no_reply`.
|
|
71
|
+
When a normal user message arrives through Onlyne, pi receives it as a follow-up message. Onlyne control messages such as `/handshake` are consumed silently. The agent can then call `onlyne_reply`, or deliberately call `onlyne_mark_no_reply`.
|
|
74
72
|
|
|
75
73
|
## Commands
|
|
76
74
|
|
|
@@ -118,7 +116,7 @@ onlyne_broadcast({
|
|
|
118
116
|
{ channelId: "telegram" },
|
|
119
117
|
{ channelId: "feishu" }
|
|
120
118
|
],
|
|
121
|
-
text: "# Release shipped\n\nVersion 0.3.
|
|
119
|
+
text: "# Release shipped\n\nVersion 0.3.2 is live."
|
|
122
120
|
})
|
|
123
121
|
```
|
|
124
122
|
|
package/SPEC.md
CHANGED
|
@@ -7,14 +7,15 @@ 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
|
-
- `watch on`
|
|
11
|
-
-
|
|
10
|
+
- `watch on` connects only to the workspace-local `.onlyne/run/onlyne.sock`; if unavailable, it tells the user to start `onlyne --workspace <root> run`.
|
|
11
|
+
- pi-onlyne never owns or launches the daemon. Users handle launchd/systemd/background scripts outside the extension, per workspace.
|
|
12
12
|
- Inbound events come from Onlyne `subscribe_events`; no polling.
|
|
13
13
|
- Inbound mode is rule-based: `auto-handle`, `queue-only`, or `muted`.
|
|
14
14
|
- Outbound defaults to `guarded-explicit`: prefer tool reply, fallback to final text, else send configured error text.
|
|
15
15
|
- Send tools default to Markdown and may pass `raw_text: true` to Onlyne for literal text.
|
|
16
16
|
- Broadcast sends concurrently with per-target retry and per-target results.
|
|
17
17
|
- Loopback inbound messages wake Pi without creating a reply obligation.
|
|
18
|
+
- `/handshake` inbound messages are Onlyne control messages and must not be surfaced to Pi as agent work.
|
|
18
19
|
- After pi surfaces an inbound follow-up, it calls `mark_io_consumed` so Onlyne FIFO `out_cursor = "consume"` stays synchronized with pi notifications.
|
|
19
20
|
- FIFO IO itself remains owned by the Onlyne daemon; pi-onlyne does not open `.onlyne/channels/*/in|out` directly.
|
|
20
21
|
|
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,
|
|
3
|
+
import { broadcast, connectDaemon, loopback, markConsumed, sendWithRetry, 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" };
|
|
@@ -10,7 +10,7 @@ function inboundText(data) { const msg = data?.data?.data ?? data?.data ?? data;
|
|
|
10
10
|
function consumeIfNotified(inbound) { if (state.workspace && inbound.messageId)
|
|
11
11
|
void markConsumed(state.workspace.socketPath, inbound.messageId).catch(() => { }); }
|
|
12
12
|
async function startWatch(pi) { state.workspace = findWorkspace(state.cwd); if (!state.workspace)
|
|
13
|
-
throw new Error("current workspace has no .onlyne configuration"); const conn = await
|
|
13
|
+
throw new Error("current workspace has no .onlyne configuration"); const conn = await connectDaemon(state.workspace); state.owner = conn.owner; state.child = conn.process; state.socket = subscribe(state.workspace.socketPath, (line) => { if (!line?.event || line.type !== "inbound_message")
|
|
14
14
|
return; const inbound = inboundText(line); if (!inbound)
|
|
15
15
|
return; const mode = inboundModeFor(currentConfig(), inbound.channelId, inbound.conversationId); if (mode === "muted")
|
|
16
16
|
return; if (inbound.channelId === "loopback") {
|
|
@@ -18,18 +18,20 @@ async function startWatch(pi) { state.workspace = findWorkspace(state.cwd); if (
|
|
|
18
18
|
pi.sendUserMessage(`Onlyne loopback activation${inbound.conversationId ? ` (${inbound.conversationId})` : ""}:\n\n${inbound.text}`, { deliverAs: "followUp" });
|
|
19
19
|
consumeIfNotified(inbound);
|
|
20
20
|
return;
|
|
21
|
+
} if (inbound.text.trim() === "/handshake") {
|
|
22
|
+
consumeIfNotified(inbound);
|
|
23
|
+
return;
|
|
21
24
|
} state.currentInbound = { ...inbound, replied: false, noReply: false, reminders: 0 }; if (mode === "auto-handle") {
|
|
22
25
|
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" });
|
|
23
26
|
consumeIfNotified(inbound);
|
|
24
27
|
} }); state.watching = true; return `watching ${state.workspace.root} (${state.owner})`; }
|
|
25
|
-
function stopWatch() { state.socket?.destroy(); state.socket = undefined;
|
|
26
|
-
stopProcess(state.child); state.child = undefined; state.watching = false; state.owner = "stopped"; return "watch stopped"; }
|
|
28
|
+
function stopWatch() { state.socket?.destroy(); state.socket = undefined; stopProcess(state.child); state.child = undefined; state.watching = false; state.owner = "stopped"; return "watch stopped"; }
|
|
27
29
|
async function reply(text) { if (!state.workspace)
|
|
28
30
|
throw new Error("onlyne workspace not found"); const inbound = state.currentInbound; if (!inbound)
|
|
29
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)
|
|
30
32
|
inbound.replied = true; return res; }
|
|
31
33
|
export default function onlyne(pi) {
|
|
32
|
-
pi.on("session_start", async (_event, ctx) => { state.cwd = ctx.cwd; state.workspace = findWorkspace(ctx.cwd); ctx.ui.setStatus("onlyne", state.workspace ? "onlyne: ready" : "onlyne: no .onlyne"); if (currentConfig().watch.autoStart && state.workspace) {
|
|
34
|
+
pi.on("session_start", async (_event, ctx) => { const resumeWatch = state.watching; 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) {
|
|
33
35
|
try {
|
|
34
36
|
ctx.ui.notify(await startWatch(pi), "info");
|
|
35
37
|
}
|
package/dist/onlyne.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
@@ -20,13 +20,12 @@ export interface SendResult extends SendTarget {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function request(socketPath: string, req: OnlyneRequest): Promise<any>;
|
|
22
22
|
export declare function subscribe(socketPath: string, onLine: (line: any) => void): Socket;
|
|
23
|
-
export declare function spawnDaemon(ws: Workspace, onlyneBin?: string): ChildProcess;
|
|
24
23
|
export declare function waitForSocket(socketPath: string, timeoutMs?: number): Promise<void>;
|
|
25
|
-
export declare function
|
|
26
|
-
owner: "external"
|
|
24
|
+
export declare function connectDaemon(ws: Workspace): Promise<{
|
|
25
|
+
owner: "external";
|
|
27
26
|
process?: ChildProcess;
|
|
28
27
|
}>;
|
|
29
|
-
export declare function stopProcess(
|
|
28
|
+
export declare function stopProcess(_child?: ChildProcess): void;
|
|
30
29
|
export declare function loopback(socketPath: string, text: string, rawText?: boolean): Promise<any>;
|
|
31
30
|
export declare function markConsumed(socketPath: string, messageId: string): Promise<any>;
|
|
32
31
|
export declare function sendWithRetry(socketPath: string, target: SendTarget, text: string, attempts: number, rawText?: boolean): Promise<SendResult>;
|
package/dist/onlyne.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { createConnection } from "node:net";
|
|
3
2
|
export function request(socketPath, req) {
|
|
4
3
|
return new Promise((resolve, reject) => {
|
|
@@ -38,20 +37,6 @@ export function subscribe(socketPath, onLine) {
|
|
|
38
37
|
} });
|
|
39
38
|
return socket;
|
|
40
39
|
}
|
|
41
|
-
export function spawnDaemon(ws, onlyneBin = process.env.ONLYNE_BIN ?? "onlyne") {
|
|
42
|
-
const script = `
|
|
43
|
-
set -eu
|
|
44
|
-
parent="$1"
|
|
45
|
-
shift
|
|
46
|
-
"$@" &
|
|
47
|
-
child=$!
|
|
48
|
-
cleanup() { kill "$child" 2>/dev/null || true; wait "$child" 2>/dev/null || true; }
|
|
49
|
-
trap cleanup INT TERM HUP EXIT
|
|
50
|
-
while kill -0 "$parent" 2>/dev/null && kill -0 "$child" 2>/dev/null; do sleep 1; done
|
|
51
|
-
cleanup
|
|
52
|
-
`;
|
|
53
|
-
return spawn("sh", ["-c", script, "onlyne-supervisor", String(process.pid), onlyneBin, "--workspace", ws.root, "run"], { cwd: ws.root, stdio: "ignore" });
|
|
54
|
-
}
|
|
55
40
|
export async function waitForSocket(socketPath, timeoutMs = 5000) {
|
|
56
41
|
const deadline = Date.now() + timeoutMs;
|
|
57
42
|
let last;
|
|
@@ -67,19 +52,16 @@ export async function waitForSocket(socketPath, timeoutMs = 5000) {
|
|
|
67
52
|
}
|
|
68
53
|
throw last instanceof Error ? last : new Error("onlyne socket not ready");
|
|
69
54
|
}
|
|
70
|
-
export async function
|
|
55
|
+
export async function connectDaemon(ws) {
|
|
71
56
|
try {
|
|
72
57
|
await request(ws.socketPath, { id: "ping", op: "ping" });
|
|
73
58
|
return { owner: "external" };
|
|
74
59
|
}
|
|
75
|
-
catch {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return { owner: "extension", process };
|
|
60
|
+
catch (e) {
|
|
61
|
+
throw new Error(`onlyne daemon is not running for ${ws.root}; start it with: onlyne --workspace ${ws.root} run`, { cause: e });
|
|
62
|
+
}
|
|
79
63
|
}
|
|
80
|
-
export function stopProcess(
|
|
81
|
-
return; child.kill("SIGTERM"); setTimeout(() => { if (!child.killed)
|
|
82
|
-
child.kill("SIGKILL"); }, 1500).unref(); }
|
|
64
|
+
export function stopProcess(_child) { }
|
|
83
65
|
export async function loopback(socketPath, text, rawText = true) {
|
|
84
66
|
return request(socketPath, { id: `loopback-${Date.now()}`, op: "loopback", text, raw_text: rawText });
|
|
85
67
|
}
|