pi-onlyne 0.2.4 → 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
@@ -26,6 +26,7 @@ With this extension, a pi agent can:
26
26
  - reply to the current inbound message
27
27
  - send a message to a specific channel conversation
28
28
  - broadcast the same message to multiple conversations
29
+ - inject a local loopback activation so background scripts can wake the session
29
30
  - mark an inbound message as intentionally not replied
30
31
 
31
32
  Messages are Markdown by default, matching normal agent output. Use `rawText: true` only when the message must be sent literally.
@@ -46,6 +47,8 @@ You also need the `onlyne` CLI installed and an initialized workspace:
46
47
 
47
48
  ```bash
48
49
  onlyne init
50
+ # Optional: refresh the workspace-local agent skill
51
+ onlyne export-skill
49
52
  ```
50
53
 
51
54
  If `onlyne` is not on `PATH`, set:
@@ -81,6 +84,7 @@ When a message arrives through Onlyne, pi receives it as a follow-up message. Th
81
84
  onlyne_reply({ text })
82
85
  onlyne_send({ channelId, conversationId, text, rawText? })
83
86
  onlyne_broadcast({ targets, text, rawText? })
87
+ onlyne_loopback({ text, conversationId?, rawText? })
84
88
  onlyne_mark_no_reply({ reason? })
85
89
  ```
86
90
 
@@ -117,6 +121,16 @@ onlyne_broadcast({
117
121
  })
118
122
  ```
119
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}'
130
+ ```
131
+
132
+ Pi treats channel `loopback` as wake-up-only: it sends a follow-up to the session, but does not expect `onlyne_reply`.
133
+
120
134
  ## Local state
121
135
 
122
136
  This extension stores its own pi-side config at:
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.4",
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",