@syengup/friday-channel-next 0.1.9 → 0.1.12

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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import crypto from "node:crypto";
7
7
  import fs from "node:fs";
8
+ import os from "node:os";
8
9
  import path from "node:path";
9
10
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
10
11
  import { createFridayNextLogger } from "./logging.js";
@@ -27,7 +28,7 @@ function pickFirstString(source, keys) {
27
28
  function resolveLocalMediaPath(mediaUrl, localRoots) {
28
29
  if (path.isAbsolute(mediaUrl))
29
30
  return mediaUrl;
30
- const roots = localRoots ?? [process.cwd(), "/tmp"];
31
+ const roots = localRoots ?? [process.cwd(), os.tmpdir()];
31
32
  for (const root of roots) {
32
33
  const candidate = path.join(root, mediaUrl);
33
34
  if (fs.existsSync(candidate))
@@ -0,0 +1,13 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export interface FridayAgentEntry {
3
+ id: string;
4
+ name?: string;
5
+ description?: string;
6
+ /** Primary model ref (e.g. "openai/gpt-4"); resolved from string or {primary} forms. */
7
+ model?: string;
8
+ thinkingDefault?: string;
9
+ isDefault: boolean;
10
+ emoji?: string;
11
+ avatar?: string;
12
+ }
13
+ export declare function handleAgentsList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,97 @@
1
+ import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
2
+ import { extractBearerToken } from "../middleware/auth.js";
3
+ const DEFAULT_AGENT_ID = "main";
4
+ /** Agent ids already in path/shell-safe form skip the slug rewrite below. */
5
+ const SAFE_AGENT_ID = /^[a-z0-9][a-z0-9_-]*$/;
6
+ /**
7
+ * Mirror of OpenClaw's `normalizeAgentId` (src/routing/session-key.ts): trim,
8
+ * lowercase, keep path/shell-safe. Empty → "main".
9
+ */
10
+ function normalizeAgentId(value) {
11
+ const trimmed = typeof value === "string" ? value.trim() : "";
12
+ if (!trimmed)
13
+ return DEFAULT_AGENT_ID;
14
+ const lowered = trimmed.toLowerCase();
15
+ if (SAFE_AGENT_ID.test(lowered))
16
+ return lowered;
17
+ return (lowered
18
+ .replace(/[^a-z0-9_-]+/g, "-")
19
+ .replace(/^-+|-+$/g, "")
20
+ .slice(0, 64) || DEFAULT_AGENT_ID);
21
+ }
22
+ /** Extract a primary model ref from the `model` field (string or {primary,...}). */
23
+ function resolvePrimaryModel(model) {
24
+ if (typeof model === "string")
25
+ return readString(model);
26
+ if (model && typeof model === "object") {
27
+ return readString(model.primary);
28
+ }
29
+ return undefined;
30
+ }
31
+ function readString(value) {
32
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
33
+ }
34
+ /**
35
+ * Reads the configured agents directly from the runtime config (same approach as
36
+ * models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
37
+ * agent, so we return a single default entry to match that behaviour.
38
+ */
39
+ function resolveConfiguredAgents() {
40
+ const rt = getFridayAgentForwardRuntime();
41
+ if (!rt)
42
+ return { agents: [], defaultAgentId: DEFAULT_AGENT_ID };
43
+ const cfg = rt.getConfig();
44
+ const agents = cfg?.agents;
45
+ const list = agents?.list;
46
+ if (!Array.isArray(list) || list.length === 0) {
47
+ return {
48
+ agents: [{ id: DEFAULT_AGENT_ID, isDefault: true }],
49
+ defaultAgentId: DEFAULT_AGENT_ID,
50
+ };
51
+ }
52
+ // Default agent: first entry marked `default: true`, else the first entry.
53
+ const explicitDefault = list.find((a) => a?.default === true);
54
+ const defaultAgentId = normalizeAgentId((explicitDefault ?? list[0])?.id);
55
+ const seen = new Set();
56
+ const entries = [];
57
+ for (const agent of list) {
58
+ if (!agent || typeof agent !== "object")
59
+ continue;
60
+ const id = normalizeAgentId(agent.id);
61
+ if (seen.has(id))
62
+ continue;
63
+ seen.add(id);
64
+ const identity = agent.identity;
65
+ entries.push({
66
+ id,
67
+ name: readString(agent.name) ?? readString(identity?.name),
68
+ description: readString(agent.description),
69
+ model: resolvePrimaryModel(agent.model),
70
+ thinkingDefault: readString(agent.thinkingDefault),
71
+ isDefault: id === defaultAgentId,
72
+ emoji: readString(identity?.emoji),
73
+ avatar: readString(identity?.avatar) ?? readString(identity?.avatarUrl),
74
+ });
75
+ }
76
+ return { agents: entries, defaultAgentId };
77
+ }
78
+ export async function handleAgentsList(req, res) {
79
+ if (req.method !== "GET") {
80
+ res.statusCode = 405;
81
+ res.setHeader("Content-Type", "application/json");
82
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
83
+ return true;
84
+ }
85
+ const token = extractBearerToken(req);
86
+ if (!token) {
87
+ res.statusCode = 401;
88
+ res.setHeader("Content-Type", "application/json");
89
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
90
+ return true;
91
+ }
92
+ const { agents, defaultAgentId } = resolveConfiguredAgents();
93
+ res.statusCode = 200;
94
+ res.setHeader("Content-Type", "application/json");
95
+ res.end(JSON.stringify({ ok: true, agents, defaultAgentId }));
96
+ return true;
97
+ }
@@ -13,6 +13,7 @@ import { handleDeviceApprove } from "./handlers/device-approve.js";
13
13
  import { handleNodesApprove } from "./handlers/nodes-approve.js";
14
14
  import { handleSessionsSettings } from "./handlers/sessions-settings.js";
15
15
  import { handleModelsList } from "./handlers/models-list.js";
16
+ import { handleAgentsList } from "./handlers/agents-list.js";
16
17
  import { handleStatus } from "./handlers/status.js";
17
18
  import { handleHealth } from "./handlers/health.js";
18
19
  import { applyCorsHeaders } from "./middleware/cors.js";
@@ -61,6 +62,9 @@ async function handleFridayNextRoute(req, res) {
61
62
  if (req.method === "GET" && pathname === "/friday-next/models") {
62
63
  return await handleModelsList(req, res);
63
64
  }
65
+ if (req.method === "GET" && pathname === "/friday-next/agents") {
66
+ return await handleAgentsList(req, res);
67
+ }
64
68
  if (req.method === "GET" && pathname === "/friday-next/status") {
65
69
  return await handleStatus(req, res);
66
70
  }
@@ -5,7 +5,7 @@ const FRIDAY_AGENT_ID = "main";
5
5
  const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
6
6
  function deriveOpenClawBaseDir(historyDir) {
7
7
  if (historyDir) {
8
- const match = historyDir.replace(/\/+$/, "").match(/(.*\/\.openclaw)\//);
8
+ const match = historyDir.replace(/[\\/]+$/, "").match(/(.*[\\/]\.openclaw)[\\/]/);
9
9
  if (match?.[1])
10
10
  return match[1];
11
11
  }
@@ -48,7 +48,7 @@ export class FridaySseOfflineQueue {
48
48
  return 0;
49
49
  let max = 0;
50
50
  const content = fs.readFileSync(file, "utf8");
51
- for (const line of content.split("\n")) {
51
+ for (const line of content.split(/\r?\n/)) {
52
52
  if (!line.trim())
53
53
  continue;
54
54
  try {
@@ -82,7 +82,7 @@ export class FridaySseOfflineQueue {
82
82
  return [];
83
83
  const out = [];
84
84
  const content = fs.readFileSync(file, "utf8");
85
- for (const line of content.split("\n")) {
85
+ for (const line of content.split(/\r?\n/)) {
86
86
  if (!line.trim())
87
87
  continue;
88
88
  try {
@@ -111,7 +111,7 @@ export class FridaySseOfflineQueue {
111
111
  return;
112
112
  const all = [];
113
113
  const content = fs.readFileSync(file, "utf8");
114
- for (const line of content.split("\n")) {
114
+ for (const line of content.split(/\r?\n/)) {
115
115
  if (!line.trim())
116
116
  continue;
117
117
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,15 @@
12
12
  "tsconfig.json",
13
13
  "openclaw.plugin.json"
14
14
  ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "prepublishOnly": "pnpm build",
18
+ "test": "npm run test:unit && npm run test:e2e",
19
+ "test:unit": "vitest run",
20
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
21
+ "test:smoke": "node scripts/e2e-smoke.mjs",
22
+ "test:msg-live": "node scripts/message-roundtrip-live.mjs"
23
+ },
15
24
  "bin": {
16
25
  "friday-channel-next": "install.js"
17
26
  },
@@ -57,13 +66,5 @@
57
66
  "typescript": "^6.0.3",
58
67
  "vitest": "^4.1.5",
59
68
  "zod": "^4.3.6"
60
- },
61
- "scripts": {
62
- "build": "tsc -p tsconfig.json",
63
- "test": "npm run test:unit && npm run test:e2e",
64
- "test:unit": "vitest run",
65
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
66
- "test:smoke": "node scripts/e2e-smoke.mjs",
67
- "test:msg-live": "node scripts/message-roundtrip-live.mjs"
68
69
  }
69
- }
70
+ }
package/src/channel.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import crypto from "node:crypto";
8
8
  import fs from "node:fs";
9
+ import os from "node:os";
9
10
  import path from "node:path";
10
11
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
11
12
  import { createFridayNextLogger } from "./logging.js";
@@ -33,7 +34,7 @@ function pickFirstString(source: Record<string, unknown>, keys: string[]): strin
33
34
 
34
35
  function resolveLocalMediaPath(mediaUrl: string, localRoots?: string[]): string {
35
36
  if (path.isAbsolute(mediaUrl)) return mediaUrl;
36
- const roots = localRoots ?? [process.cwd(), "/tmp"];
37
+ const roots = localRoots ?? [process.cwd(), os.tmpdir()];
37
38
  for (const root of roots) {
38
39
  const candidate = path.join(root, mediaUrl);
39
40
  if (fs.existsSync(candidate)) return candidate;
@@ -0,0 +1,101 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { createAppSimulator } from "../test-support/app-simulator.js";
3
+ import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ setFridayAgentForwardRuntime,
6
+ resetFridayAgentForwardRuntimeForTest,
7
+ } from "../agent-forward-runtime.js";
8
+
9
+ /**
10
+ * Inject a fake host config into the forward runtime, which is what
11
+ * agents-list reads via getFridayAgentForwardRuntime().getConfig().
12
+ * setMockRuntime only wires the auth/runtime config, not the forward runtime.
13
+ */
14
+ function setForwardConfig(config: unknown): void {
15
+ setFridayAgentForwardRuntime({
16
+ runtime: {
17
+ agent: { session: { resolveStorePath: () => "", loadSessionStore: () => ({}) } },
18
+ config: { current: () => config },
19
+ },
20
+ } as never);
21
+ }
22
+
23
+ async function getAgents(app: ReturnType<typeof createAppSimulator>, headers?: Record<string, string>) {
24
+ const res = await app.rawRequest({ method: "GET", path: "/friday-next/agents", headers });
25
+ return { status: res.status, body: res.body ? JSON.parse(res.body) : {}, headers: res.headers };
26
+ }
27
+
28
+ describe("e2e agents list", () => {
29
+ let historyDir = "";
30
+
31
+ beforeEach(() => {
32
+ historyDir = createTempHistoryDir();
33
+ });
34
+
35
+ afterEach(() => {
36
+ resetFridayAgentForwardRuntimeForTest();
37
+ removeTempHistoryDir(historyDir);
38
+ });
39
+
40
+ it("rejects a bad bearer token with 401", async () => {
41
+ setMockRuntime({ historyDir, authToken: "test-token" });
42
+ setForwardConfig({ agents: { list: [{ id: "main" }] } });
43
+ const app = createAppSimulator({ token: "test-token" });
44
+
45
+ const res = await getAgents(app, { authorization: "Bearer wrong-token" });
46
+ expect(res.status).toBe(401);
47
+ });
48
+
49
+ it("returns an implicit main agent when none are configured", async () => {
50
+ setMockRuntime({ historyDir, authToken: "test-token" });
51
+ setForwardConfig({ agents: { defaults: {} } });
52
+ const app = createAppSimulator({ token: "test-token" });
53
+
54
+ const res = await getAgents(app);
55
+ expect(res.status).toBe(200);
56
+ expect(res.body.ok).toBe(true);
57
+ expect(res.body.defaultAgentId).toBe("main");
58
+ expect(res.body.agents).toEqual([{ id: "main", isDefault: true }]);
59
+ });
60
+
61
+ it("lists configured agents end-to-end with normalized ids and default selection", async () => {
62
+ setMockRuntime({ historyDir, authToken: "test-token" });
63
+ setForwardConfig({
64
+ agents: {
65
+ list: [
66
+ { id: "Main", name: "Primary", model: "openai/gpt-4", thinkingDefault: "medium" },
67
+ {
68
+ id: "Research Bot",
69
+ description: "deep research",
70
+ model: { primary: "anthropic/claude", fallbacks: ["x"] },
71
+ identity: { emoji: "🔬", avatar: "data:img" },
72
+ default: true,
73
+ },
74
+ ],
75
+ },
76
+ });
77
+ const app = createAppSimulator({ token: "test-token" });
78
+
79
+ const res = await getAgents(app);
80
+ expect(res.status).toBe(200);
81
+ expect(res.headers["content-type"]).toContain("application/json");
82
+ expect(res.body.defaultAgentId).toBe("research-bot");
83
+ expect(res.body.agents).toEqual([
84
+ {
85
+ id: "main",
86
+ name: "Primary",
87
+ model: "openai/gpt-4",
88
+ thinkingDefault: "medium",
89
+ isDefault: false,
90
+ },
91
+ {
92
+ id: "research-bot",
93
+ description: "deep research",
94
+ model: "anthropic/claude",
95
+ isDefault: true,
96
+ emoji: "🔬",
97
+ avatar: "data:img",
98
+ },
99
+ ]);
100
+ });
101
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { handleAgentsList } from "./agents-list.js";
4
+ import { setMockRuntime } from "../../test-support/mock-runtime.js";
5
+ import {
6
+ setFridayAgentForwardRuntime,
7
+ resetFridayAgentForwardRuntimeForTest,
8
+ } from "../../agent-forward-runtime.js";
9
+
10
+ class MockRes extends EventEmitter {
11
+ statusCode = 0;
12
+ headers: Record<string, string> = {};
13
+ body = "";
14
+ setHeader(name: string, value: string): void {
15
+ this.headers[name.toLowerCase()] = value;
16
+ }
17
+ end(body?: string): void {
18
+ if (body) this.body += body;
19
+ }
20
+ }
21
+
22
+ function makeReq(headers: Record<string, string> = {}, method = "GET"): any {
23
+ return { method, url: "/friday-next/agents", headers };
24
+ }
25
+
26
+ const AUTH = { authorization: "Bearer test-token" };
27
+
28
+ /** Inject a fake config into the forward runtime (handler reads getConfig()). */
29
+ function setConfig(config: unknown): void {
30
+ setFridayAgentForwardRuntime({
31
+ runtime: {
32
+ agent: { session: { resolveStorePath: () => "", loadSessionStore: () => ({}) } },
33
+ config: { current: () => config },
34
+ },
35
+ } as any);
36
+ }
37
+
38
+ describe("handleAgentsList", () => {
39
+ beforeEach(() => {
40
+ setMockRuntime();
41
+ });
42
+
43
+ afterEach(() => {
44
+ resetFridayAgentForwardRuntimeForTest();
45
+ });
46
+
47
+ it("rejects non-GET methods with 405", async () => {
48
+ const res = new MockRes();
49
+ await handleAgentsList(makeReq(AUTH, "POST"), res as any);
50
+ expect(res.statusCode).toBe(405);
51
+ });
52
+
53
+ it("rejects missing/invalid bearer token with 401", async () => {
54
+ const res = new MockRes();
55
+ await handleAgentsList(makeReq(), res as any);
56
+ expect(res.statusCode).toBe(401);
57
+ });
58
+
59
+ it("returns an implicit main agent when none are configured", async () => {
60
+ setConfig({ agents: { defaults: {} } });
61
+ const res = new MockRes();
62
+ await handleAgentsList(makeReq(AUTH), res as any);
63
+
64
+ expect(res.statusCode).toBe(200);
65
+ const body = JSON.parse(res.body);
66
+ expect(body.ok).toBe(true);
67
+ expect(body.defaultAgentId).toBe("main");
68
+ expect(body.agents).toEqual([{ id: "main", isDefault: true }]);
69
+ });
70
+
71
+ it("lists configured agents with normalized ids and resolved fields", async () => {
72
+ setConfig({
73
+ agents: {
74
+ list: [
75
+ { id: "Main", name: "Primary", model: "openai/gpt-4", thinkingDefault: "medium" },
76
+ {
77
+ id: "Research Bot",
78
+ description: "deep research",
79
+ model: { primary: "anthropic/claude", fallbacks: ["x"] },
80
+ identity: { emoji: "🔬", avatar: "data:..." },
81
+ default: true,
82
+ },
83
+ ],
84
+ },
85
+ });
86
+ const res = new MockRes();
87
+ await handleAgentsList(makeReq(AUTH), res as any);
88
+
89
+ expect(res.statusCode).toBe(200);
90
+ const body = JSON.parse(res.body);
91
+ expect(body.defaultAgentId).toBe("research-bot");
92
+ expect(body.agents).toEqual([
93
+ {
94
+ id: "main",
95
+ name: "Primary",
96
+ model: "openai/gpt-4",
97
+ thinkingDefault: "medium",
98
+ isDefault: false,
99
+ },
100
+ {
101
+ id: "research-bot",
102
+ description: "deep research",
103
+ model: "anthropic/claude",
104
+ isDefault: true,
105
+ emoji: "🔬",
106
+ avatar: "data:...",
107
+ },
108
+ ]);
109
+ });
110
+
111
+ it("defaults to the first entry when none is marked default and dedups ids", async () => {
112
+ setConfig({
113
+ agents: {
114
+ list: [
115
+ { id: "alpha" },
116
+ { id: "alpha", name: "dup" },
117
+ { id: "beta" },
118
+ ],
119
+ },
120
+ });
121
+ const res = new MockRes();
122
+ await handleAgentsList(makeReq(AUTH), res as any);
123
+
124
+ const body = JSON.parse(res.body);
125
+ expect(body.defaultAgentId).toBe("alpha");
126
+ expect(body.agents.map((a: { id: string }) => a.id)).toEqual(["alpha", "beta"]);
127
+ expect(body.agents[0].isDefault).toBe(true);
128
+ });
129
+ });
@@ -0,0 +1,130 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+
5
+ const DEFAULT_AGENT_ID = "main";
6
+
7
+ /** Agent ids already in path/shell-safe form skip the slug rewrite below. */
8
+ const SAFE_AGENT_ID = /^[a-z0-9][a-z0-9_-]*$/;
9
+
10
+ export interface FridayAgentEntry {
11
+ id: string;
12
+ name?: string;
13
+ description?: string;
14
+ /** Primary model ref (e.g. "openai/gpt-4"); resolved from string or {primary} forms. */
15
+ model?: string;
16
+ thinkingDefault?: string;
17
+ isDefault: boolean;
18
+ emoji?: string;
19
+ avatar?: string;
20
+ }
21
+
22
+ interface ResolvedAgents {
23
+ agents: FridayAgentEntry[];
24
+ defaultAgentId: string;
25
+ }
26
+
27
+ /**
28
+ * Mirror of OpenClaw's `normalizeAgentId` (src/routing/session-key.ts): trim,
29
+ * lowercase, keep path/shell-safe. Empty → "main".
30
+ */
31
+ function normalizeAgentId(value: unknown): string {
32
+ const trimmed = typeof value === "string" ? value.trim() : "";
33
+ if (!trimmed) return DEFAULT_AGENT_ID;
34
+ const lowered = trimmed.toLowerCase();
35
+ if (SAFE_AGENT_ID.test(lowered)) return lowered;
36
+ return (
37
+ lowered
38
+ .replace(/[^a-z0-9_-]+/g, "-")
39
+ .replace(/^-+|-+$/g, "")
40
+ .slice(0, 64) || DEFAULT_AGENT_ID
41
+ );
42
+ }
43
+
44
+ /** Extract a primary model ref from the `model` field (string or {primary,...}). */
45
+ function resolvePrimaryModel(model: unknown): string | undefined {
46
+ if (typeof model === "string") return readString(model);
47
+ if (model && typeof model === "object") {
48
+ return readString((model as Record<string, unknown>).primary);
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ function readString(value: unknown): string | undefined {
54
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
55
+ }
56
+
57
+ /**
58
+ * Reads the configured agents directly from the runtime config (same approach as
59
+ * models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
60
+ * agent, so we return a single default entry to match that behaviour.
61
+ */
62
+ function resolveConfiguredAgents(): ResolvedAgents {
63
+ const rt = getFridayAgentForwardRuntime();
64
+ if (!rt) return { agents: [], defaultAgentId: DEFAULT_AGENT_ID };
65
+
66
+ const cfg = rt.getConfig() as Record<string, unknown>;
67
+ const agents = cfg?.agents as Record<string, unknown> | undefined;
68
+ const list = agents?.list as Array<Record<string, unknown>> | undefined;
69
+
70
+ if (!Array.isArray(list) || list.length === 0) {
71
+ return {
72
+ agents: [{ id: DEFAULT_AGENT_ID, isDefault: true }],
73
+ defaultAgentId: DEFAULT_AGENT_ID,
74
+ };
75
+ }
76
+
77
+ // Default agent: first entry marked `default: true`, else the first entry.
78
+ const explicitDefault = list.find((a) => a?.default === true);
79
+ const defaultAgentId = normalizeAgentId((explicitDefault ?? list[0])?.id);
80
+
81
+ const seen = new Set<string>();
82
+ const entries: FridayAgentEntry[] = [];
83
+ for (const agent of list) {
84
+ if (!agent || typeof agent !== "object") continue;
85
+ const id = normalizeAgentId(agent.id);
86
+ if (seen.has(id)) continue;
87
+ seen.add(id);
88
+
89
+ const identity = agent.identity as Record<string, unknown> | undefined;
90
+ entries.push({
91
+ id,
92
+ name: readString(agent.name) ?? readString(identity?.name),
93
+ description: readString(agent.description),
94
+ model: resolvePrimaryModel(agent.model),
95
+ thinkingDefault: readString(agent.thinkingDefault),
96
+ isDefault: id === defaultAgentId,
97
+ emoji: readString(identity?.emoji),
98
+ avatar: readString(identity?.avatar) ?? readString(identity?.avatarUrl),
99
+ });
100
+ }
101
+
102
+ return { agents: entries, defaultAgentId };
103
+ }
104
+
105
+ export async function handleAgentsList(
106
+ req: IncomingMessage,
107
+ res: ServerResponse,
108
+ ): Promise<boolean> {
109
+ if (req.method !== "GET") {
110
+ res.statusCode = 405;
111
+ res.setHeader("Content-Type", "application/json");
112
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
113
+ return true;
114
+ }
115
+
116
+ const token = extractBearerToken(req);
117
+ if (!token) {
118
+ res.statusCode = 401;
119
+ res.setHeader("Content-Type", "application/json");
120
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
121
+ return true;
122
+ }
123
+
124
+ const { agents, defaultAgentId } = resolveConfiguredAgents();
125
+
126
+ res.statusCode = 200;
127
+ res.setHeader("Content-Type", "application/json");
128
+ res.end(JSON.stringify({ ok: true, agents, defaultAgentId }));
129
+ return true;
130
+ }
@@ -15,6 +15,7 @@ import { handleDeviceApprove } from "./handlers/device-approve.js";
15
15
  import { handleNodesApprove } from "./handlers/nodes-approve.js";
16
16
  import { handleSessionsSettings } from "./handlers/sessions-settings.js";
17
17
  import { handleModelsList } from "./handlers/models-list.js";
18
+ import { handleAgentsList } from "./handlers/agents-list.js";
18
19
  import { handleStatus } from "./handlers/status.js";
19
20
  import { handleHealth } from "./handlers/health.js";
20
21
  import { applyCorsHeaders } from "./middleware/cors.js";
@@ -77,6 +78,10 @@ async function handleFridayNextRoute(
77
78
  return await handleModelsList(req, res);
78
79
  }
79
80
 
81
+ if (req.method === "GET" && pathname === "/friday-next/agents") {
82
+ return await handleAgentsList(req, res);
83
+ }
84
+
80
85
  if (req.method === "GET" && pathname === "/friday-next/status") {
81
86
  return await handleStatus(req, res);
82
87
  }
@@ -7,7 +7,7 @@ const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
7
7
 
8
8
  function deriveOpenClawBaseDir(historyDir?: string): string {
9
9
  if (historyDir) {
10
- const match = historyDir.replace(/\/+$/, "").match(/(.*\/\.openclaw)\//);
10
+ const match = historyDir.replace(/[\\/]+$/, "").match(/(.*[\\/]\.openclaw)[\\/]/);
11
11
  if (match?.[1]) return match[1];
12
12
  }
13
13
  return join(os.homedir(), ".openclaw");
@@ -55,7 +55,7 @@ export class FridaySseOfflineQueue {
55
55
  if (!fs.existsSync(file)) return 0;
56
56
  let max = 0;
57
57
  const content = fs.readFileSync(file, "utf8");
58
- for (const line of content.split("\n")) {
58
+ for (const line of content.split(/\r?\n/)) {
59
59
  if (!line.trim()) continue;
60
60
  try {
61
61
  const o = JSON.parse(line) as { id?: number };
@@ -87,7 +87,7 @@ export class FridaySseOfflineQueue {
87
87
  if (!fs.existsSync(file)) return [];
88
88
  const out: PersistedSseEntry[] = [];
89
89
  const content = fs.readFileSync(file, "utf8");
90
- for (const line of content.split("\n")) {
90
+ for (const line of content.split(/\r?\n/)) {
91
91
  if (!line.trim()) continue;
92
92
  try {
93
93
  const o = JSON.parse(line) as PersistedSseEntry;
@@ -115,7 +115,7 @@ export class FridaySseOfflineQueue {
115
115
  if (!fs.existsSync(file)) return;
116
116
  const all: PersistedSseEntry[] = [];
117
117
  const content = fs.readFileSync(file, "utf8");
118
- for (const line of content.split("\n")) {
118
+ for (const line of content.split(/\r?\n/)) {
119
119
  if (!line.trim()) continue;
120
120
  try {
121
121
  const o = JSON.parse(line) as PersistedSseEntry;