pi-onlyne 0.2.3 → 0.2.5

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 CHANGED
@@ -1,14 +1,35 @@
1
1
  # pi-onlyne
2
2
 
3
- Pi extension for using Onlyne as a workspace-local messaging bridge.
3
+ **Give pi agents a real IM inbox/outbox through [Onlyne](https://github.com/dbydd/onlyne).**
4
4
 
5
- ## What it does
5
+ `pi-onlyne` is the Pi extension for Onlyne. It adds tools and commands to pi so an agent can receive messages from IM channels and send replies without pretending that a chat platform is a terminal, a browser tab, or a custom workflow engine.
6
6
 
7
- - Watches an existing Onlyne workspace.
8
- - Starts Onlyne for the current workspace when requested.
9
- - Subscribes to inbound channel events without polling.
10
- - Lets the agent reply, send, or broadcast messages through tools.
11
- - Keeps config in the project, not in global home state.
7
+ ## What is Onlyne?
8
+
9
+ [Onlyne](https://github.com/dbydd/onlyne) is a small workspace-local IM channel daemon. It runs in your project directory, keeps its config/state under `.onlyne/`, and brokers local agent calls to real messaging adapters such as Telegram, Feishu/Lark, QQ Bot, and WeChat.
10
+
11
+ Onlyne is deliberately narrow:
12
+
13
+ - local workspace daemon, not a global cloud service
14
+ - channel broker, not an agent runtime
15
+ - Unix socket / stdio friendly, not a web dashboard
16
+ - local history and event stream, not a heavy message platform
17
+
18
+ ## What does this extension do?
19
+
20
+ `pi-onlyne` connects pi to an existing Onlyne workspace and exposes Onlyne as native pi tools.
21
+
22
+ With this extension, a pi agent can:
23
+
24
+ - watch an Onlyne workspace for inbound IM messages
25
+ - surface inbound messages into the current pi session
26
+ - reply to the current inbound message
27
+ - send a message to a specific channel conversation
28
+ - broadcast the same message to multiple conversations
29
+ - inject a local loopback activation so background scripts can wake the session
30
+ - mark an inbound message as intentionally not replied
31
+
32
+ Messages are Markdown by default, matching normal agent output. Use `rawText: true` only when the message must be sent literally.
12
33
 
13
34
  ## Install
14
35
 
@@ -16,16 +37,37 @@ Pi extension for using Onlyne as a workspace-local messaging bridge.
16
37
  pi install npm:pi-onlyne
17
38
  ```
18
39
 
19
- For a one-off run:
40
+ For a one-off run without installing:
20
41
 
21
42
  ```bash
22
43
  pi -e npm:pi-onlyne
23
44
  ```
24
45
 
25
- ## Requirements
46
+ You also need the `onlyne` CLI installed and an initialized workspace:
47
+
48
+ ```bash
49
+ onlyne init
50
+ # Optional: refresh the workspace-local agent skill
51
+ onlyne export-skill
52
+ ```
53
+
54
+ If `onlyne` is not on `PATH`, set:
55
+
56
+ ```bash
57
+ export ONLYNE_BIN=/path/to/onlyne
58
+ ```
59
+
60
+ ## Typical workflow
26
61
 
27
- - `onlyne` available on `PATH`, or set `ONLYNE_BIN`.
28
- - A workspace with `.onlyne/` already initialized.
62
+ 1. Initialize/configure Onlyne in your project.
63
+ 2. Install this Pi extension.
64
+ 3. Start watching from pi:
65
+
66
+ ```text
67
+ /onlyne watch on
68
+ ```
69
+
70
+ 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`.
29
71
 
30
72
  ## Commands
31
73
 
@@ -42,13 +84,70 @@ pi -e npm:pi-onlyne
42
84
  onlyne_reply({ text })
43
85
  onlyne_send({ channelId, conversationId, text, rawText? })
44
86
  onlyne_broadcast({ targets, text, rawText? })
45
- onlyne_mark_no_reply({ reason })
87
+ onlyne_loopback({ text, conversationId?, rawText? })
88
+ onlyne_mark_no_reply({ reason? })
89
+ ```
90
+
91
+ ### Send one message
92
+
93
+ ```ts
94
+ onlyne_send({
95
+ channelId: "telegram",
96
+ conversationId: "123456",
97
+ text: "# Build report\n\nAll checks passed."
98
+ })
99
+ ```
100
+
101
+ ### Send literal text
102
+
103
+ ```ts
104
+ onlyne_send({
105
+ channelId: "telegram",
106
+ conversationId: "123456",
107
+ text: "# not a heading",
108
+ rawText: true
109
+ })
110
+ ```
111
+
112
+ ### Broadcast
113
+
114
+ ```ts
115
+ onlyne_broadcast({
116
+ targets: [
117
+ { channelId: "telegram", conversationId: "123456" },
118
+ { channelId: "feishu", conversationId: "oc_xxx" }
119
+ ],
120
+ text: "# Release shipped\n\nVersion 0.2.3 is live."
121
+ })
122
+ ```
123
+
124
+ ### Loopback wake-up
125
+
126
+ From any local script, inject an inbound message into the current Onlyne daemon:
127
+
128
+ ```bash
129
+ onlyne client '{"id":"wake","op":"loopback","text":"background job finished","raw_text":true}'
46
130
  ```
47
131
 
48
- Messages default to Markdown. Set `rawText: true` only when the text must be sent literally.
132
+ Pi treats channel `loopback` as wake-up-only: it sends a follow-up to the session, but does not expect `onlyne_reply`.
133
+
134
+ ## Local state
135
+
136
+ This extension stores its own pi-side config at:
137
+
138
+ ```text
139
+ .pi/onlyne.json
140
+ ```
141
+
142
+ Onlyne itself stores workspace state under:
143
+
144
+ ```text
145
+ .onlyne/
146
+ ```
49
147
 
50
- ## Config
148
+ That keeps each project isolated: different workspaces can run different Onlyne daemons, channels, histories, and policies.
51
149
 
52
- Project-local config lives at `.pi/onlyne.json`. Defaults are safe: watch is manual, inbound messages auto-handle once watch is on, and outbound reply fallback is guarded.
150
+ ## Links
53
151
 
54
- See `SPEC.md` for behavior details.
152
+ - Onlyne main repository: https://github.com/dbydd/onlyne
153
+ - pi-onlyne package: https://www.npmjs.com/package/pi-onlyne
package/SPEC.md CHANGED
@@ -14,6 +14,7 @@ Pi extension for Onlyne. Onlyne remains a workspace-local IM broker; this extens
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
+ - Loopback inbound messages wake Pi without creating a reply obligation.
17
18
 
18
19
  ## Config
19
20
 
@@ -36,6 +37,7 @@ Stored in project `.pi/onlyne.json`:
36
37
  - `onlyne_reply({ text })`
37
38
  - `onlyne_send({ channelId, conversationId, text, rawText? })`
38
39
  - `onlyne_broadcast({ targets, text, rawText? })`
40
+ - `onlyne_loopback({ text, conversationId?, rawText? })`
39
41
  - `onlyne_mark_no_reply({ reason? })`
40
42
 
41
43
  ## Deferred
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, connectOrSpawn, sendWithRetry, stopProcess, subscribe } from "./onlyne.js";
3
+ import { broadcast, connectOrSpawn, loopback, 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" };
@@ -11,7 +11,11 @@ async function startWatch(pi) { state.workspace = findWorkspace(state.cwd); if (
11
11
  throw new Error("current workspace has no .onlyne configuration"); const conn = await connectOrSpawn(state.workspace); state.owner = conn.owner; state.child = conn.process; state.socket = subscribe(state.workspace.socketPath, (line) => { if (!line?.event || line.type !== "inbound_message")
12
12
  return; const inbound = inboundText(line); if (!inbound)
13
13
  return; const mode = inboundModeFor(currentConfig(), inbound.channelId, inbound.conversationId); if (mode === "muted")
14
- return; state.currentInbound = { ...inbound, replied: false, noReply: false, reminders: 0 }; if (mode === "auto-handle")
14
+ return; if (inbound.channelId === "loopback") {
15
+ if (mode === "auto-handle")
16
+ pi.sendUserMessage(`Onlyne loopback activation${inbound.conversationId ? ` (${inbound.conversationId})` : ""}:\n\n${inbound.text}`, { deliverAs: "followUp" });
17
+ return;
18
+ } state.currentInbound = { ...inbound, replied: false, noReply: false, reminders: 0 }; if (mode === "auto-handle")
15
19
  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" }); }); state.watching = true; return `watching ${state.workspace.root} (${state.owner})`; }
16
20
  function stopWatch() { state.socket?.destroy(); state.socket = undefined; if (state.owner === "extension")
17
21
  stopProcess(state.child); state.child = undefined; state.watching = false; state.owner = "stopped"; return "watch stopped"; }
@@ -73,6 +77,8 @@ export default function onlyne(pi) {
73
77
  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); } }));
74
78
  pi.registerTool(defineTool({ name: "onlyne_broadcast", label: "Onlyne broadcast", description: "Send Markdown to many Onlyne channel conversations concurrently. Set rawText=true only for literal plain text.", parameters: Type.Object({ targets: Type.Array(Type.Object({ channelId: Type.String(), conversationId: Type.String() })), text: Type.String(), rawText: Type.Optional(Type.Boolean()) }), executionMode: "parallel", async execute(_id, params) { if (!state.workspace)
75
79
  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); } }));
80
+ 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.", parameters: Type.Object({ text: Type.String(), conversationId: Type.Optional(Type.String()), rawText: Type.Optional(Type.Boolean()) }), executionMode: "parallel", async execute(_id, params) { if (!state.workspace)
81
+ throw new Error("onlyne workspace not found"); const res = await loopback(state.workspace.socketPath, params.text, params.conversationId ?? "self", params.rawText ?? true); return textResult(JSON.stringify(res), res); } }));
76
82
  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)
77
83
  state.currentInbound.noReply = true; return textResult("marked no reply", params); } }));
78
84
  }
package/dist/onlyne.d.ts CHANGED
@@ -28,5 +28,6 @@ export declare function connectOrSpawn(ws: Workspace): Promise<{
28
28
  process?: ChildProcess;
29
29
  }>;
30
30
  export declare function stopProcess(child?: ChildProcess): void;
31
+ export declare function loopback(socketPath: string, text: string, conversationId?: string, rawText?: boolean): Promise<any>;
31
32
  export declare function sendWithRetry(socketPath: string, target: SendTarget, text: string, attempts: number, rawText?: boolean): Promise<SendResult>;
32
33
  export declare function broadcast(socketPath: string, targets: SendTarget[], text: string, attempts: number, concurrency: number, rawText?: boolean): Promise<SendResult[]>;
package/dist/onlyne.js CHANGED
@@ -80,6 +80,9 @@ export async function connectOrSpawn(ws) {
80
80
  export function stopProcess(child) { if (!child || child.killed)
81
81
  return; child.kill("SIGTERM"); setTimeout(() => { if (!child.killed)
82
82
  child.kill("SIGKILL"); }, 1500).unref(); }
83
+ export async function loopback(socketPath, text, conversationId = "self", rawText = true) {
84
+ return request(socketPath, { id: `loopback-${Date.now()}`, op: "loopback", conversation_id: conversationId, text, raw_text: rawText });
85
+ }
83
86
  export async function sendWithRetry(socketPath, target, text, attempts, rawText = false) {
84
87
  let error = "unknown error";
85
88
  for (let i = 0; i < Math.max(1, attempts); i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onlyne",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Pi extension tools for sending messages through Onlyne.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",