pi-onlyne 0.3.1 → 0.3.4

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
@@ -46,25 +46,23 @@ For a one-off run without installing:
46
46
  pi -e npm:pi-onlyne
47
47
  ```
48
48
 
49
- You also need the `onlyne` CLI installed and an initialized workspace:
49
+ You also need an initialized Onlyne workspace and a running workspace-local daemon:
50
50
 
51
51
  ```bash
52
52
  onlyne init
53
- # Optional: refresh the workspace-local agent skill
53
+ onlyne run
54
+ # Optional, in another shell: refresh the workspace-local agent skill
54
55
  onlyne export-skill
55
56
  ```
56
57
 
57
- If `onlyne` is not on `PATH`, set:
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. Install this Pi extension.
67
- 3. Start watching from pi:
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
@@ -81,6 +79,8 @@ When a normal user message arrives through Onlyne, pi receives it as a follow-up
81
79
  /onlyne config auto-start
82
80
  ```
83
81
 
82
+ `/onlyne` supports argument completions for `status`, `watch on`, `watch off`, and `config auto-start`.
83
+
84
84
  ## Agent tools
85
85
 
86
86
  ```text
@@ -118,7 +118,7 @@ onlyne_broadcast({
118
118
  { channelId: "telegram" },
119
119
  { channelId: "feishu" }
120
120
  ],
121
- text: "# Release shipped\n\nVersion 0.3.1 is live."
121
+ text: "# Release shipped\n\nVersion 0.3.4 is live."
122
122
  })
123
123
  ```
124
124
 
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
- - `watch on` first connects to existing `.onlyne/run/onlyne.sock`; if unavailable, spawns `onlyne --workspace <root> run`.
11
- - Extension-owned daemon is killed on `watch off`, `session_shutdown`, or process signal.
10
+ - `/onlyne` provides argument completions for its supported subcommands.
11
+ - `watch on` connects only to the workspace-local `.onlyne/run/onlyne.sock`; if unavailable, it tells the user to start `onlyne --workspace <root> run`.
12
+ - pi-onlyne never owns or launches the daemon. Users handle launchd/systemd/background scripts outside the extension, per workspace.
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, connectOrSpawn, loopback, markConsumed, sendWithRetry, stopProcess, subscribe } from "./onlyne.js";
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 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")
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") {
@@ -25,8 +25,7 @@ async function startWatch(pi) { state.workspace = findWorkspace(state.cwd); if (
25
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" });
26
26
  consumeIfNotified(inbound);
27
27
  } }); state.watching = true; return `watching ${state.workspace.root} (${state.owner})`; }
28
- function stopWatch() { state.socket?.destroy(); state.socket = undefined; if (state.owner === "extension")
29
- 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"; }
30
29
  async function reply(text) { if (!state.workspace)
31
30
  throw new Error("onlyne workspace not found"); const inbound = state.currentInbound; if (!inbound)
32
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)
@@ -61,25 +60,37 @@ export default function onlyne(pi) {
61
60
  }
62
61
  await reply(inbound.fallbackText || state.lastValidOutput || cfg.outbound.guardedExplicit.noOutputFallbackText);
63
62
  });
64
- pi.registerCommand("onlyne", { description: "Onlyne watch/status/config commands", handler: async (argLine, ctx) => { const [cmd, sub] = argLine.trim().split(/\s+/); try {
65
- if (cmd === "watch" && sub === "on")
66
- ctx.ui.notify(await startWatch(pi), "info");
67
- else if (cmd === "watch" && sub === "off")
68
- ctx.ui.notify(stopWatch(), "info");
69
- else if (cmd === "status")
70
- ctx.ui.notify(`onlyne ${state.watching ? "watching" : "stopped"}; owner=${state.owner}; workspace=${state.workspace?.root ?? "none"}`, "info");
71
- else if (cmd === "config" && sub === "auto-start") {
72
- const cfg = currentConfig();
73
- cfg.watch.autoStart = !cfg.watch.autoStart;
74
- saveConfig(state.cwd, cfg);
75
- ctx.ui.notify(`autoStart=${cfg.watch.autoStart}`, "info");
63
+ pi.registerCommand("onlyne", {
64
+ description: "Onlyne watch/status/config commands",
65
+ getArgumentCompletions: (prefix) => {
66
+ const commands = ["status", "watch on", "watch off", "config auto-start"];
67
+ const p = prefix.trimStart();
68
+ const filtered = commands.filter((c) => c.startsWith(p));
69
+ return filtered.length ? filtered.map((value) => ({ value, label: value })) : null;
70
+ },
71
+ handler: async (argLine, ctx) => {
72
+ const [cmd, sub] = argLine.trim().split(/\s+/);
73
+ try {
74
+ if (cmd === "watch" && sub === "on")
75
+ ctx.ui.notify(await startWatch(pi), "info");
76
+ else if (cmd === "watch" && sub === "off")
77
+ ctx.ui.notify(stopWatch(), "info");
78
+ else if (cmd === "status")
79
+ ctx.ui.notify(`onlyne ${state.watching ? "watching" : "stopped"}; owner=${state.owner}; workspace=${state.workspace?.root ?? "none"}`, "info");
80
+ else if (cmd === "config" && sub === "auto-start") {
81
+ const cfg = currentConfig();
82
+ cfg.watch.autoStart = !cfg.watch.autoStart;
83
+ saveConfig(state.cwd, cfg);
84
+ ctx.ui.notify(`autoStart=${cfg.watch.autoStart}`, "info");
85
+ }
86
+ else
87
+ ctx.ui.notify("usage: /onlyne status | watch on|off | config auto-start", "info");
76
88
  }
77
- else
78
- ctx.ui.notify("usage: /onlyne status | watch on|off | config auto-start", "info");
79
- }
80
- catch (e) {
81
- ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
82
- } } });
89
+ catch (e) {
90
+ ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
91
+ }
92
+ },
93
+ });
83
94
  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))); } }));
84
95
  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)
85
96
  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); } }));
package/dist/onlyne.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ChildProcess } from "node:child_process";
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 connectOrSpawn(ws: Workspace): Promise<{
26
- owner: "external" | "extension";
24
+ export declare function connectDaemon(ws: Workspace): Promise<{
25
+ owner: "external";
27
26
  process?: ChildProcess;
28
27
  }>;
29
- export declare function stopProcess(child?: ChildProcess): void;
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 connectOrSpawn(ws) {
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 { /* spawn */ }
76
- const process = spawnDaemon(ws);
77
- await waitForSocket(ws.socketPath);
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(child) { if (!child || child.killed)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onlyne",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Pi extension tools for sending messages through Onlyne.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",