@syengup/friday-channel-next 0.0.1
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 +35 -0
- package/index.ts +191 -0
- package/install.mjs +158 -0
- package/install.sh +118 -0
- package/openclaw.plugin.json +53 -0
- package/package.json +65 -0
- package/src/agent/abort-run.ts +10 -0
- package/src/agent/active-runs.ts +26 -0
- package/src/agent/dispatch-bridge.ts +18 -0
- package/src/agent/media-bridge.ts +23 -0
- package/src/agent-forward-runtime.ts +30 -0
- package/src/agent-run-context-bridge.ts +32 -0
- package/src/channel-actions.ts +129 -0
- package/src/channel.ts +284 -0
- package/src/collect-message-media-paths.ts +132 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +64 -0
- package/src/e2e/attachments-inbound.e2e.test.ts +43 -0
- package/src/e2e/attachments-outbound.e2e.test.ts +43 -0
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +56 -0
- package/src/e2e/connect-and-connected.e2e.test.ts +44 -0
- package/src/e2e/offline-replay.e2e.test.ts +43 -0
- package/src/e2e/send-text.e2e.test.ts +73 -0
- package/src/e2e/slash-commands.e2e.test.ts +33 -0
- package/src/e2e/status-cors-auth.e2e.test.ts +41 -0
- package/src/e2e/tool-lifecycle.e2e.test.ts +49 -0
- package/src/friday-inbound-stats.ts +10 -0
- package/src/friday-session.forward-agent.test.ts +270 -0
- package/src/friday-session.ts +327 -0
- package/src/host-config.ts +20 -0
- package/src/http/handlers/cancel.test.ts +70 -0
- package/src/http/handlers/cancel.ts +35 -0
- package/src/http/handlers/files-download.ts +239 -0
- package/src/http/handlers/files-upload.ts +166 -0
- package/src/http/handlers/files.ts +335 -0
- package/src/http/handlers/messages.test.ts +119 -0
- package/src/http/handlers/messages.ts +555 -0
- package/src/http/handlers/models-list.ts +126 -0
- package/src/http/handlers/sessions-delete.ts +59 -0
- package/src/http/handlers/sessions-settings.ts +90 -0
- package/src/http/handlers/sse.test.ts +71 -0
- package/src/http/handlers/sse.ts +84 -0
- package/src/http/handlers/status.test.ts +52 -0
- package/src/http/handlers/status.ts +33 -0
- package/src/http/middleware/auth.test.ts +46 -0
- package/src/http/middleware/auth.ts +31 -0
- package/src/http/middleware/body.test.ts +27 -0
- package/src/http/middleware/body.ts +28 -0
- package/src/http/middleware/cors.test.ts +40 -0
- package/src/http/middleware/cors.ts +12 -0
- package/src/http/server.ts +106 -0
- package/src/logging.ts +27 -0
- package/src/openclaw.d.ts +32 -0
- package/src/run-metadata.ts +180 -0
- package/src/runtime.ts +14 -0
- package/src/session/session-manager.ts +230 -0
- package/src/session-usage-snapshot.ts +80 -0
- package/src/sse/emitter.test.ts +85 -0
- package/src/sse/emitter.ts +249 -0
- package/src/sse/frame-format.test.ts +56 -0
- package/src/sse/offline-queue.test.ts +65 -0
- package/src/sse/offline-queue.ts +140 -0
- package/src/test-support/app-simulator.ts +243 -0
- package/src/test-support/mock-dispatch.ts +181 -0
- package/src/test-support/mock-runtime.ts +74 -0
- package/src/vendor/runtime-store.ts +99 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Friday Next Channel Plugin
|
|
2
|
+
|
|
3
|
+
`friday-next` is an OpenClaw channel plugin for Apple apps (iOS/macOS) using HTTP + standard SSE.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Channel id: `friday-next` (alongside legacy `friday` if configured)
|
|
8
|
+
- **Transparent proxy SSE**: forwards OpenClaw `onAgentEvent` as `event: agent`, dispatch `deliver` as `event: deliver`, tool hooks as `event: tool-hook`, channel pushes as `event: outbound`
|
|
9
|
+
- **Single synthetic event**: `connected` (`deviceId`, `serverTime`, `lastSeq`)
|
|
10
|
+
- **Disk-backed SSE replay** per `deviceId` (JSONL + `Last-Event-ID` / `lastEventId`) for offline gaps and restarts
|
|
11
|
+
- File upload/download (`POST /friday-next/files`, `GET /friday-next/files/:id`)
|
|
12
|
+
- Cancel (`POST /friday-next/cancel`) and status with `activeRuns` (`GET /friday-next/status`)
|
|
13
|
+
|
|
14
|
+
## Endpoints
|
|
15
|
+
|
|
16
|
+
- `GET /friday-next/events?deviceId=...`
|
|
17
|
+
- `POST /friday-next/messages`
|
|
18
|
+
- `POST /friday-next/files`
|
|
19
|
+
- `GET /friday-next/files/:id`
|
|
20
|
+
- `POST /friday-next/cancel`
|
|
21
|
+
- `GET /friday-next/status`
|
|
22
|
+
|
|
23
|
+
See **`API.md`** (English) and **`API.zh-CN.md`** (Chinese) for payloads, event shapes, offline queue paths, and breaking changes.
|
|
24
|
+
|
|
25
|
+
## Testing
|
|
26
|
+
|
|
27
|
+
- `pnpm test:unit` — Vitest unit tests (excludes `*.e2e.test.ts`)
|
|
28
|
+
- `pnpm test:e2e` — in-process app simulator (`vitest.e2e.config.ts`)
|
|
29
|
+
- `pnpm test` — `test:unit` then `test:e2e`
|
|
30
|
+
- `pnpm test:smoke` — optional live gateway smoke (gateway running; token from env or config)
|
|
31
|
+
|
|
32
|
+
## Migration
|
|
33
|
+
|
|
34
|
+
- There is **no** `GET/DELETE /friday-next/history`; clients must reconstruct state from SSE.
|
|
35
|
+
- Legacy SSE names (`final`, `run-start`, `run-complete`, `run-error`, `reasoning`, `attachment`, `tts`, `block`, …) are **not** emitted; use `agent` + `deliver` + `tool-hook` + `outbound`.
|
package/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
|
+
import type { PluginHookBeforeToolCallEvent, PluginHookAfterToolCallEvent, PluginHookToolContext } from "openclaw/plugin-sdk/plugins/types";
|
|
5
|
+
import { fridayNextChannelPlugin } from "./src/channel.js";
|
|
6
|
+
import { setFridayNextRuntime } from "./src/runtime.js";
|
|
7
|
+
import { resolveFridayNextConfig } from "./src/config.js";
|
|
8
|
+
import { getHostOpenClawConfigSnapshot } from "./src/host-config.js";
|
|
9
|
+
import { registerFridayNextHttpRoutes } from "./src/http/server.js";
|
|
10
|
+
import { getFridayNextRuntime } from "./src/runtime.js";
|
|
11
|
+
import { sseEmitter } from "./src/sse/emitter.js";
|
|
12
|
+
import {
|
|
13
|
+
forwardAgentEventRaw,
|
|
14
|
+
getLastRegisteredFridayDeviceId,
|
|
15
|
+
resolveFridayDeviceIdForSessionKey,
|
|
16
|
+
} from "./src/friday-session.js";
|
|
17
|
+
import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
|
|
18
|
+
import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
|
|
19
|
+
|
|
20
|
+
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
21
|
+
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
22
|
+
|
|
23
|
+
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
24
|
+
let fridayNextToolHooksRegistered = false;
|
|
25
|
+
let disposeAgentEventListener: (() => void) | null = null;
|
|
26
|
+
/** Avoid duplicate `registerHttpRoute` when gateway re-invokes `registerFull`. */
|
|
27
|
+
let fridayNextPluginHttpRegistered = false;
|
|
28
|
+
|
|
29
|
+
function deviceIdFromToolContext(ctx: PluginHookToolContext): string | null {
|
|
30
|
+
if (ctx.runId) {
|
|
31
|
+
const d = sseEmitter.getDeviceIdByRunId(ctx.runId);
|
|
32
|
+
if (d) return d;
|
|
33
|
+
}
|
|
34
|
+
const sk =
|
|
35
|
+
typeof ctx.sessionKey === "string" && ctx.sessionKey.trim()
|
|
36
|
+
? ctx.sessionKey.trim()
|
|
37
|
+
: (ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "";
|
|
38
|
+
if (sk) {
|
|
39
|
+
const d = resolveFridayDeviceIdForSessionKey(sk);
|
|
40
|
+
if (d) return d;
|
|
41
|
+
}
|
|
42
|
+
const sole = sseEmitter.getSoleConnectedDeviceId();
|
|
43
|
+
if (sole) return sole;
|
|
44
|
+
const last = getLastRegisteredFridayDeviceId();
|
|
45
|
+
if (last) return last;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isFridaySessionKey(sk: string): boolean {
|
|
50
|
+
return /^friday-next-/i.test(sk) || /^agent:main:friday-next-/i.test(sk);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldForwardToolEventToFriday(ctx: PluginHookToolContext): boolean {
|
|
54
|
+
if (ctx.runId) {
|
|
55
|
+
if (sseEmitter.getDeviceIdByRunId(ctx.runId)) return true;
|
|
56
|
+
const runSk = getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() ?? "";
|
|
57
|
+
if (runSk) {
|
|
58
|
+
if (resolveFridayDeviceIdForSessionKey(runSk)) return true;
|
|
59
|
+
if (isFridaySessionKey(runSk)) return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sk = typeof ctx.sessionKey === "string" ? ctx.sessionKey.trim() : "";
|
|
64
|
+
if (sk) {
|
|
65
|
+
if (resolveFridayDeviceIdForSessionKey(sk)) return true;
|
|
66
|
+
if (isFridaySessionKey(sk)) return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default defineChannelPluginEntry({
|
|
73
|
+
id: "friday-next",
|
|
74
|
+
name: "Friday Next",
|
|
75
|
+
description: "Friday Next Apple 应用通道",
|
|
76
|
+
plugin: fridayNextChannelPlugin as ChannelPlugin,
|
|
77
|
+
setRuntime: setFridayNextRuntime,
|
|
78
|
+
registerFull: (api: OpenClawPluginApi) => {
|
|
79
|
+
setFridayAgentForwardRuntime(api);
|
|
80
|
+
if (!fridayNextPluginHttpRegistered) {
|
|
81
|
+
fridayNextPluginHttpRegistered = true;
|
|
82
|
+
registerFridayNextHttpRoutes(api);
|
|
83
|
+
} else {
|
|
84
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
85
|
+
sseEmitter.setBacklogLimit(cfg.sseBacklogPerDevice);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disposeAgentEventListener?.();
|
|
89
|
+
disposeAgentEventListener = api.runtime.events.onAgentEvent((evt: any) => {
|
|
90
|
+
forwardAgentEventRaw({
|
|
91
|
+
runId: evt.runId,
|
|
92
|
+
seq: evt.seq,
|
|
93
|
+
ts: evt.ts,
|
|
94
|
+
stream: evt.stream as string,
|
|
95
|
+
data: evt.data as Record<string, unknown>,
|
|
96
|
+
sessionKey: evt.sessionKey,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (fridayNextToolHooksRegistered) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
fridayNextToolHooksRegistered = true;
|
|
104
|
+
|
|
105
|
+
api.on("subagent_delivery_target", (event: any) => {
|
|
106
|
+
if (!event.expectsCompletionMessage) return;
|
|
107
|
+
const ch = event.requesterOrigin?.channel?.trim().toLowerCase();
|
|
108
|
+
if (ch !== "friday-next") return;
|
|
109
|
+
const sk = event.requesterSessionKey?.trim();
|
|
110
|
+
if (!sk) return;
|
|
111
|
+
const raw = resolveFridayDeviceIdForSessionKey(sk);
|
|
112
|
+
if (!raw) return;
|
|
113
|
+
const to = raw.toUpperCase();
|
|
114
|
+
return {
|
|
115
|
+
origin: {
|
|
116
|
+
channel: "friday-next",
|
|
117
|
+
accountId: event.requesterOrigin?.accountId?.trim() || "default",
|
|
118
|
+
to,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
api.on("before_tool_call", (event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => {
|
|
124
|
+
if (!shouldForwardToolEventToFriday(ctx)) return;
|
|
125
|
+
const deviceId = deviceIdFromToolContext(ctx);
|
|
126
|
+
const runId = ctx.runId ?? "(unknown)";
|
|
127
|
+
|
|
128
|
+
const logLine = (detail: string) => {
|
|
129
|
+
const ts = new Date().toISOString();
|
|
130
|
+
console.error(
|
|
131
|
+
`[Friday-HOOK] [${ts}] [TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (!deviceId) {
|
|
136
|
+
logLine("SKIP_no_deviceId");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logLine("START");
|
|
141
|
+
sseEmitter.broadcastToolEvent(deviceId.toUpperCase(), runId, {
|
|
142
|
+
type: "tool-hook",
|
|
143
|
+
data: {
|
|
144
|
+
when: "before",
|
|
145
|
+
runId,
|
|
146
|
+
deviceId: deviceId.toUpperCase(),
|
|
147
|
+
sessionKey: ctx.sessionKey,
|
|
148
|
+
toolName: event.toolName,
|
|
149
|
+
params: event.params,
|
|
150
|
+
ts: Date.now(),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
api.on("after_tool_call", (event: PluginHookAfterToolCallEvent, ctx: PluginHookToolContext) => {
|
|
156
|
+
if (!shouldForwardToolEventToFriday(ctx)) return;
|
|
157
|
+
const deviceId = deviceIdFromToolContext(ctx);
|
|
158
|
+
const runId = ctx.runId ?? "(unknown)";
|
|
159
|
+
|
|
160
|
+
const logLine = (detail: string) => {
|
|
161
|
+
const ts = new Date().toISOString();
|
|
162
|
+
console.error(
|
|
163
|
+
`[Friday-HOOK] [${ts}] [TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (!deviceId) {
|
|
168
|
+
logLine("SKIP_no_deviceId");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logLine("END");
|
|
173
|
+
const normalizedDeviceId = deviceId.toUpperCase();
|
|
174
|
+
sseEmitter.broadcastToolEvent(normalizedDeviceId, runId, {
|
|
175
|
+
type: "tool-hook",
|
|
176
|
+
data: {
|
|
177
|
+
when: "after",
|
|
178
|
+
runId,
|
|
179
|
+
deviceId: normalizedDeviceId,
|
|
180
|
+
sessionKey: ctx.sessionKey,
|
|
181
|
+
toolName: event.toolName,
|
|
182
|
+
toolCallId: event.toolCallId,
|
|
183
|
+
error: event.error ?? null,
|
|
184
|
+
result: event.result,
|
|
185
|
+
durationMs: event.durationMs ?? null,
|
|
186
|
+
ts: Date.now(),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
});
|
package/install.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PLUGIN_DIR = process.argv[2] || join(homedir(), ".openclaw", "extensions", "friday-channel-next");
|
|
12
|
+
const OPENCLAW_CONFIG = join(homedir(), ".openclaw", "openclaw.json");
|
|
13
|
+
const REPO_URL = process.env.FRIDAY_NEXT_REPO || "https://github.com/SyengUp/openclaw-fridaynext-channel.git";
|
|
14
|
+
|
|
15
|
+
const G = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
16
|
+
const Y = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
17
|
+
const R = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
18
|
+
|
|
19
|
+
function log(msg) {
|
|
20
|
+
console.log(`${G("[friday-next]")} ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
function warn(msg) {
|
|
23
|
+
console.log(`${Y("[friday-next]")} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
function err(msg) {
|
|
26
|
+
console.error(`${R("[friday-next]")} ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function has(cmd) {
|
|
30
|
+
try {
|
|
31
|
+
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Running from an npm/npx package when we have the full source (index.ts + package.json)
|
|
39
|
+
// and are NOT already inside the target plugin dir.
|
|
40
|
+
function isRunningFromNpmPackage() {
|
|
41
|
+
return (
|
|
42
|
+
resolve(__dirname) !== resolve(PLUGIN_DIR) &&
|
|
43
|
+
existsSync(join(__dirname, "package.json")) &&
|
|
44
|
+
existsSync(join(__dirname, "index.ts"))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --------------- prerequisites ---------------
|
|
49
|
+
|
|
50
|
+
const required = ["pnpm", "node", "openclaw"];
|
|
51
|
+
const missing = required.filter((c) => !has(c));
|
|
52
|
+
if (missing.length) {
|
|
53
|
+
missing.forEach((c) => err(`${c} is required but not found. Install it first.`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!existsSync(OPENCLAW_CONFIG)) {
|
|
58
|
+
err(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
|
|
59
|
+
err("Make sure OpenClaw is installed and has been run at least once.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --------------- acquire source ---------------
|
|
64
|
+
|
|
65
|
+
if (existsSync(PLUGIN_DIR)) {
|
|
66
|
+
log(`Plugin directory found: ${PLUGIN_DIR}`);
|
|
67
|
+
} else if (isRunningFromNpmPackage()) {
|
|
68
|
+
log(`Copying plugin from npm package to ${PLUGIN_DIR} ...`);
|
|
69
|
+
cpSync(__dirname, PLUGIN_DIR, {
|
|
70
|
+
recursive: true,
|
|
71
|
+
filter: (src) => {
|
|
72
|
+
const rel = relative(__dirname, src);
|
|
73
|
+
if (rel === "") return true; // root dir
|
|
74
|
+
const top = rel.split(sep)[0];
|
|
75
|
+
return ![".git", "node_modules", "dist", "attachments", ".claude"].includes(top);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
if (!has("git")) {
|
|
80
|
+
err("git is required for installation from GitHub. Install git first or use npx @openclaw/friday-channel-next.");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
log(`Cloning plugin to ${PLUGIN_DIR} ...`);
|
|
84
|
+
execSync(`git clone "${REPO_URL}" "${PLUGIN_DIR}"`, { stdio: "inherit" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.chdir(PLUGIN_DIR);
|
|
88
|
+
|
|
89
|
+
// --------------- install + build ---------------
|
|
90
|
+
|
|
91
|
+
log("Installing dependencies...");
|
|
92
|
+
try {
|
|
93
|
+
execSync("pnpm install --frozen-lockfile", { stdio: "inherit" });
|
|
94
|
+
} catch {
|
|
95
|
+
execSync("pnpm install", { stdio: "inherit" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log("Building TypeScript...");
|
|
99
|
+
execSync("pnpm build", { stdio: "inherit" });
|
|
100
|
+
|
|
101
|
+
// --------------- configure OpenClaw ---------------
|
|
102
|
+
|
|
103
|
+
log("Configuring OpenClaw...");
|
|
104
|
+
|
|
105
|
+
const config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf8"));
|
|
106
|
+
|
|
107
|
+
if (!config.plugins) config.plugins = {};
|
|
108
|
+
if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
|
|
109
|
+
if (!config.plugins.allow.includes("friday-next")) {
|
|
110
|
+
config.plugins.allow.push("friday-next");
|
|
111
|
+
console.log(" + Added friday-next to plugins.allow");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
115
|
+
if (!config.plugins.entries["friday-next"]) {
|
|
116
|
+
config.plugins.entries["friday-next"] = { enabled: true };
|
|
117
|
+
console.log(" + Added friday-next to plugins.entries (enabled)");
|
|
118
|
+
} else if (!config.plugins.entries["friday-next"].enabled) {
|
|
119
|
+
config.plugins.entries["friday-next"].enabled = true;
|
|
120
|
+
console.log(" + Enabled friday-next in plugins.entries");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!config.channels) config.channels = {};
|
|
124
|
+
if (!config.channels["friday-next"]) {
|
|
125
|
+
config.channels["friday-next"] = { enabled: true, transport: "http+sse" };
|
|
126
|
+
console.log(" + Added friday-next channel config (auth defaults to gateway token)");
|
|
127
|
+
} else {
|
|
128
|
+
if (!config.channels["friday-next"].enabled) {
|
|
129
|
+
config.channels["friday-next"].enabled = true;
|
|
130
|
+
console.log(" + Enabled friday-next channel");
|
|
131
|
+
}
|
|
132
|
+
if (!config.channels["friday-next"].transport) {
|
|
133
|
+
config.channels["friday-next"].transport = "http+sse";
|
|
134
|
+
console.log(" + Set friday-next transport to http+sse");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!config.gateway) config.gateway = {};
|
|
139
|
+
if (config.gateway.bind !== "lan") {
|
|
140
|
+
config.gateway.bind = "lan";
|
|
141
|
+
console.log(" + Set gateway.bind to lan");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
145
|
+
console.log(" Config updated.");
|
|
146
|
+
|
|
147
|
+
// --------------- restart gateway ---------------
|
|
148
|
+
|
|
149
|
+
log("Restarting OpenClaw gateway...");
|
|
150
|
+
execSync("openclaw gateway restart", { stdio: "inherit" });
|
|
151
|
+
|
|
152
|
+
log("--------------------------------------------------");
|
|
153
|
+
log("Installation complete! Friday Next channel is now active.");
|
|
154
|
+
log("");
|
|
155
|
+
log("The channel uses your gateway auth token by default.");
|
|
156
|
+
log("To use a different token, set FRIDAY_NEXT_AUTH_TOKEN env var or");
|
|
157
|
+
log("add authToken to channels.friday-next in openclaw.json.");
|
|
158
|
+
log("--------------------------------------------------");
|
package/install.sh
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Friday Next plugin installer — one-click setup for the friday-next channel
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./install.sh [plugin-dir]
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/SyengUp/openclaw-fridaynext-channel/main/install.sh | bash
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
PLUGIN_DIR="${1:-$HOME/.openclaw/extensions/friday-channel-next}"
|
|
11
|
+
OPENCLAW_CONFIG="$HOME/.openclaw/openclaw.json"
|
|
12
|
+
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
log() { printf "%b%s\\n" "${GREEN}[friday-next]${NC} " "$1"; }
|
|
19
|
+
warn() { printf "%b%s\\n" "${YELLOW}[friday-next]${NC} " "$1"; }
|
|
20
|
+
err() { printf "%b%s\\n" "${RED}[friday-next]${NC} " "$1" >&2; }
|
|
21
|
+
|
|
22
|
+
trap 'err "Install failed."' ERR
|
|
23
|
+
|
|
24
|
+
# Check prerequisites
|
|
25
|
+
for cmd in pnpm node git openclaw; do
|
|
26
|
+
if ! command -v "$cmd" &>/dev/null; then
|
|
27
|
+
err "$cmd is required but not found. Install it first."
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$OPENCLAW_CONFIG" ]; then
|
|
33
|
+
err "OpenClaw config not found at $OPENCLAW_CONFIG"
|
|
34
|
+
err "Make sure OpenClaw is installed and has been run at least once."
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Step 1: Clone (if needed), install deps, and build
|
|
39
|
+
|
|
40
|
+
if [ -d "$PLUGIN_DIR" ]; then
|
|
41
|
+
log "Plugin directory found: $PLUGIN_DIR"
|
|
42
|
+
else
|
|
43
|
+
log "Cloning plugin to $PLUGIN_DIR ..."
|
|
44
|
+
REPO_URL="${FRIDAY_NEXT_REPO:-https://github.com/SyengUp/openclaw-fridaynext-channel.git}"
|
|
45
|
+
git clone "$REPO_URL" "$PLUGIN_DIR"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
cd "$PLUGIN_DIR"
|
|
49
|
+
|
|
50
|
+
log "Installing dependencies..."
|
|
51
|
+
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
|
52
|
+
|
|
53
|
+
log "Building TypeScript..."
|
|
54
|
+
pnpm build
|
|
55
|
+
|
|
56
|
+
# Step 2: Configure OpenClaw
|
|
57
|
+
|
|
58
|
+
log "Configuring OpenClaw..."
|
|
59
|
+
|
|
60
|
+
node --input-type=module -e '
|
|
61
|
+
import fs from "node:fs";
|
|
62
|
+
|
|
63
|
+
const configPath = process.argv[1];
|
|
64
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
65
|
+
|
|
66
|
+
if (!config.plugins) config.plugins = {};
|
|
67
|
+
if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
|
|
68
|
+
if (!config.plugins.allow.includes("friday-next")) {
|
|
69
|
+
config.plugins.allow.push("friday-next");
|
|
70
|
+
console.log(" + Added friday-next to plugins.allow");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
74
|
+
if (!config.plugins.entries["friday-next"]) {
|
|
75
|
+
config.plugins.entries["friday-next"] = { enabled: true };
|
|
76
|
+
console.log(" + Added friday-next to plugins.entries (enabled)");
|
|
77
|
+
} else if (!config.plugins.entries["friday-next"].enabled) {
|
|
78
|
+
config.plugins.entries["friday-next"].enabled = true;
|
|
79
|
+
console.log(" + Enabled friday-next in plugins.entries");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!config.channels) config.channels = {};
|
|
83
|
+
if (!config.channels["friday-next"]) {
|
|
84
|
+
config.channels["friday-next"] = { enabled: true, transport: "http+sse" };
|
|
85
|
+
console.log(" + Added friday-next channel config (auth defaults to gateway token)");
|
|
86
|
+
} else {
|
|
87
|
+
if (!config.channels["friday-next"].enabled) {
|
|
88
|
+
config.channels["friday-next"].enabled = true;
|
|
89
|
+
console.log(" + Enabled friday-next channel");
|
|
90
|
+
}
|
|
91
|
+
if (!config.channels["friday-next"].transport) {
|
|
92
|
+
config.channels["friday-next"].transport = "http+sse";
|
|
93
|
+
console.log(" + Set friday-next transport to http+sse");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!config.gateway) config.gateway = {};
|
|
98
|
+
if (config.gateway.bind !== "lan") {
|
|
99
|
+
config.gateway.bind = "lan";
|
|
100
|
+
console.log(" + Set gateway.bind to lan");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
104
|
+
console.log(" Config updated.");
|
|
105
|
+
' "$OPENCLAW_CONFIG"
|
|
106
|
+
|
|
107
|
+
# Step 3: Restart gateway
|
|
108
|
+
|
|
109
|
+
log "Restarting OpenClaw gateway..."
|
|
110
|
+
openclaw gateway restart
|
|
111
|
+
|
|
112
|
+
log "--------------------------------------------------"
|
|
113
|
+
log "Installation complete! Friday Next channel is now active."
|
|
114
|
+
log ""
|
|
115
|
+
log "The channel uses your gateway auth token by default."
|
|
116
|
+
log "To use a different token, set FRIDAY_NEXT_AUTH_TOKEN env var or"
|
|
117
|
+
log "add authToken to channels.friday-next in openclaw.json."
|
|
118
|
+
log "--------------------------------------------------"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "friday-next",
|
|
3
|
+
"channels": ["friday-next"],
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {}
|
|
8
|
+
},
|
|
9
|
+
"channelConfigs": {
|
|
10
|
+
"friday-next": {
|
|
11
|
+
"label": "Friday Next",
|
|
12
|
+
"description": "HTTP+SSE channel for the Friday iOS client.",
|
|
13
|
+
"schema": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"properties": {
|
|
17
|
+
"enabled": { "type": "boolean" },
|
|
18
|
+
"transport": { "type": "string" },
|
|
19
|
+
"pathPrefix": { "type": "string" },
|
|
20
|
+
"historyLimit": { "type": "integer", "minimum": 1, "maximum": 200 },
|
|
21
|
+
"historyDir": { "type": "string" },
|
|
22
|
+
"logLevel": { "type": "string", "enum": ["debug", "info", "warn", "error"] },
|
|
23
|
+
"authToken": { "type": "string" },
|
|
24
|
+
"cors": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"properties": {
|
|
28
|
+
"enabled": { "type": "boolean" },
|
|
29
|
+
"allowOrigin": { "type": "string" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"sse": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"properties": {
|
|
36
|
+
"keepaliveSec": { "type": "integer", "minimum": 5, "maximum": 120 },
|
|
37
|
+
"backlogPerDevice": { "type": "integer", "minimum": 0, "maximum": 1000 }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"uiHints": {
|
|
43
|
+
"authToken": {
|
|
44
|
+
"label": "Bearer auth token",
|
|
45
|
+
"sensitive": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"channelEnvVars": {
|
|
51
|
+
"friday-next": ["FRIDAY_NEXT_AUTH_TOKEN"]
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syengup/friday-channel-next",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.ts",
|
|
8
|
+
"src/",
|
|
9
|
+
"install.mjs",
|
|
10
|
+
"install.sh",
|
|
11
|
+
"tsconfig.json",
|
|
12
|
+
"pnpm-lock.yaml",
|
|
13
|
+
"openclaw.plugin.json"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"test": "pnpm test:unit && pnpm test:e2e",
|
|
18
|
+
"test:unit": "vitest run",
|
|
19
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
20
|
+
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
21
|
+
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"install-friday-next": "./install.mjs"
|
|
25
|
+
},
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"openclaw": {
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./index.ts"
|
|
39
|
+
],
|
|
40
|
+
"channel": {
|
|
41
|
+
"id": "friday-next",
|
|
42
|
+
"label": "Friday Next",
|
|
43
|
+
"selectionLabel": "Friday Next (Apple App)",
|
|
44
|
+
"detailLabel": "Friday Next Apple App",
|
|
45
|
+
"docsPath": "/channels/friday-next",
|
|
46
|
+
"docsLabel": "friday-next",
|
|
47
|
+
"blurb": "Apple app channel with HTTP + SSE v2 streaming.",
|
|
48
|
+
"systemImage": "iphone.radiowaves.left.and.right",
|
|
49
|
+
"markdownCapable": true
|
|
50
|
+
},
|
|
51
|
+
"bundle": {
|
|
52
|
+
"stageRuntimeDependencies": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^25.6.0",
|
|
57
|
+
"chalk": "^5.6.2",
|
|
58
|
+
"jiti": "^2.6.1",
|
|
59
|
+
"json5": "^2.2.3",
|
|
60
|
+
"tslog": "^4.10.2",
|
|
61
|
+
"typescript": "^6.0.3",
|
|
62
|
+
"vitest": "^4.1.5",
|
|
63
|
+
"zod": "^4.3.6"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function abortRun(runId: string): Promise<void> {
|
|
2
|
+
if (process.env.VITEST !== "true") {
|
|
3
|
+
try {
|
|
4
|
+
const { abortAgentHarnessRun } = await import("openclaw/plugin-sdk/agent-harness");
|
|
5
|
+
abortAgentHarnessRun(runId);
|
|
6
|
+
} catch {
|
|
7
|
+
// optional at runtime
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Tracks agent runs that have emitted lifecycle `phase: start` without matching `end`/`error`. */
|
|
2
|
+
|
|
3
|
+
const active = new Set<string>();
|
|
4
|
+
|
|
5
|
+
export function observeAgentEventForActiveRuns(evt: {
|
|
6
|
+
stream: string;
|
|
7
|
+
runId: string;
|
|
8
|
+
data: Record<string, unknown>;
|
|
9
|
+
}): void {
|
|
10
|
+
if (evt.stream !== "lifecycle") return;
|
|
11
|
+
const phase = evt.data.phase;
|
|
12
|
+
if (phase === "start") active.add(evt.runId);
|
|
13
|
+
if (phase === "end" || phase === "error") active.delete(evt.runId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getActiveRunIds(): string[] {
|
|
17
|
+
return [...active];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getActiveRunCount(): number {
|
|
21
|
+
return active.size;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resetActiveRunsForTest(): void {
|
|
25
|
+
active.clear();
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type DispatchFn = (args: unknown) => Promise<unknown> | unknown;
|
|
2
|
+
|
|
3
|
+
let overrideDispatch: DispatchFn | null = null;
|
|
4
|
+
|
|
5
|
+
export function runFridayDispatch(args: Parameters<DispatchFn>[0]): ReturnType<DispatchFn> {
|
|
6
|
+
if (overrideDispatch) return overrideDispatch(args);
|
|
7
|
+
return import("openclaw/plugin-sdk/reply-dispatch-runtime").then((m) =>
|
|
8
|
+
m.dispatchReplyWithDispatcher(args as never),
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function __setMockFridayDispatchForTests(fn: DispatchFn): void {
|
|
13
|
+
overrideDispatch = fn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function __resetMockFridayDispatchForTests(): void {
|
|
17
|
+
overrideDispatch = null;
|
|
18
|
+
}
|