@syengup/friday-channel-next 0.1.21 → 0.1.22

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.
@@ -14,6 +14,8 @@ export type FridayAgentForwardRuntime = {
14
14
  sessionKey: string;
15
15
  update: (entry: Record<string, unknown>) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
16
16
  }) => Promise<Record<string, unknown> | null>;
17
+ /** Resolves an agent's workspace dir — used to read IDENTITY.md for the name fallback. */
18
+ resolveAgentWorkspaceDir?: (cfg: unknown, agentId: string) => string;
17
19
  getConfig: () => unknown;
18
20
  };
19
21
  /** Called from `registerFull` so terminal lifecycle forwards can read `sessions.json` after persist. */
@@ -6,6 +6,8 @@ export function setFridayAgentForwardRuntime(api) {
6
6
  loadSessionStore: api.runtime.agent.session.loadSessionStore,
7
7
  updateSessionStoreEntry: api.runtime.agent.session
8
8
  .updateSessionStoreEntry,
9
+ resolveAgentWorkspaceDir: api.runtime.agent
10
+ .resolveAgentWorkspaceDir,
9
11
  getConfig: () => api.runtime.config.current(),
10
12
  };
11
13
  }
@@ -10,4 +10,12 @@ export interface FridayAgentEntry {
10
10
  emoji?: string;
11
11
  avatar?: string;
12
12
  }
13
+ /**
14
+ * Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
15
+ * `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
16
+ * drop the leading "- ", split on the first ":", strip markdown emphasis, and
17
+ * skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
18
+ * "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
19
+ */
20
+ export declare function parseIdentityNameFromMarkdown(content: string): string | undefined;
13
21
  export declare function handleAgentsList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,4 +1,6 @@
1
- import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getFridayAgentForwardRuntime, } from "../../agent-forward-runtime.js";
2
4
  import { extractBearerToken } from "../middleware/auth.js";
3
5
  const DEFAULT_AGENT_ID = "main";
4
6
  /** Agent ids already in path/shell-safe form skip the slug rewrite below. */
@@ -31,6 +33,60 @@ function resolvePrimaryModel(model) {
31
33
  function readString(value) {
32
34
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
33
35
  }
36
+ /** Unfilled IDENTITY.md template prompts that must not surface as a real name. */
37
+ const IDENTITY_NAME_PLACEHOLDERS = new Set(["pick something you like"]);
38
+ /**
39
+ * Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
40
+ * `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
41
+ * drop the leading "- ", split on the first ":", strip markdown emphasis, and
42
+ * skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
43
+ * "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
44
+ */
45
+ export function parseIdentityNameFromMarkdown(content) {
46
+ for (const rawLine of content.split(/\r?\n/)) {
47
+ const cleaned = rawLine.trim().replace(/^\s*-\s*/, "");
48
+ const colonIndex = cleaned.indexOf(":");
49
+ if (colonIndex === -1)
50
+ continue;
51
+ const label = cleaned.slice(0, colonIndex).replace(/[*_`]/g, "").trim().toLowerCase();
52
+ if (label !== "name")
53
+ continue;
54
+ const value = cleaned
55
+ .slice(colonIndex + 1)
56
+ .replace(/^[*_`\s]+|[*_`\s]+$/g, "")
57
+ .trim();
58
+ if (!value)
59
+ continue;
60
+ let normalized = value.replace(/[–—]/g, "-");
61
+ if (normalized.startsWith("(") && normalized.endsWith(")")) {
62
+ normalized = normalized.slice(1, -1).trim();
63
+ }
64
+ if (IDENTITY_NAME_PLACEHOLDERS.has(normalized.toLowerCase()))
65
+ continue;
66
+ return value;
67
+ }
68
+ return undefined;
69
+ }
70
+ /**
71
+ * Name fallback for agents with no `name`/`identity.name` in config (e.g. the
72
+ * implicit `main`): resolve the agent's workspace and parse its IDENTITY.md, the
73
+ * same source ControlUI reads. Best-effort — any failure yields undefined.
74
+ */
75
+ function readWorkspaceIdentityName(rt, cfg, agentId) {
76
+ const resolveWorkspace = rt.resolveAgentWorkspaceDir;
77
+ if (!resolveWorkspace)
78
+ return undefined;
79
+ try {
80
+ const workspace = resolveWorkspace(cfg, agentId);
81
+ if (!workspace)
82
+ return undefined;
83
+ const content = fs.readFileSync(path.join(workspace, "IDENTITY.md"), "utf-8");
84
+ return parseIdentityNameFromMarkdown(content);
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
34
90
  /**
35
91
  * Reads the configured agents directly from the runtime config (same approach as
36
92
  * models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
@@ -64,7 +120,9 @@ function resolveConfiguredAgents() {
64
120
  const identity = agent.identity;
65
121
  entries.push({
66
122
  id,
67
- name: readString(agent.name) ?? readString(identity?.name),
123
+ name: readString(agent.name) ??
124
+ readString(identity?.name) ??
125
+ readWorkspaceIdentityName(rt, cfg, id),
68
126
  description: readString(agent.description),
69
127
  model: resolvePrimaryModel(agent.model),
70
128
  thinkingDefault: readString(agent.thinkingDefault),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,8 @@ export type FridayAgentForwardRuntime = {
14
14
  entry: Record<string, unknown>,
15
15
  ) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
16
16
  }) => Promise<Record<string, unknown> | null>;
17
+ /** Resolves an agent's workspace dir — used to read IDENTITY.md for the name fallback. */
18
+ resolveAgentWorkspaceDir?: (cfg: unknown, agentId: string) => string;
17
19
  getConfig: () => unknown;
18
20
  };
19
21
 
@@ -26,6 +28,8 @@ export function setFridayAgentForwardRuntime(api: OpenClawPluginApi): void {
26
28
  loadSessionStore: api.runtime.agent.session.loadSessionStore,
27
29
  updateSessionStoreEntry: (api.runtime.agent.session as Record<string, unknown>)
28
30
  .updateSessionStoreEntry as FridayAgentForwardRuntime["updateSessionStoreEntry"],
31
+ resolveAgentWorkspaceDir: (api.runtime.agent as Record<string, unknown>)
32
+ .resolveAgentWorkspaceDir as FridayAgentForwardRuntime["resolveAgentWorkspaceDir"],
29
33
  getConfig: () => api.runtime.config.current(),
30
34
  };
31
35
  }
@@ -1,6 +1,9 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { EventEmitter } from "node:events";
3
- import { handleAgentsList } from "./agents-list.js";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { handleAgentsList, parseIdentityNameFromMarkdown } from "./agents-list.js";
4
7
  import { setMockRuntime } from "../../test-support/mock-runtime.js";
5
8
  import {
6
9
  setFridayAgentForwardRuntime,
@@ -108,6 +111,33 @@ describe("handleAgentsList", () => {
108
111
  ]);
109
112
  });
110
113
 
114
+ it("falls back to the IDENTITY.md name when config has none", async () => {
115
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "friday-identity-"));
116
+ fs.writeFileSync(
117
+ path.join(workspace, "IDENTITY.md"),
118
+ "# IDENTITY.md\n\n- **Name:** 星期五 (Friday)\n- **Emoji:** 🌿\n",
119
+ );
120
+ try {
121
+ setFridayAgentForwardRuntime({
122
+ runtime: {
123
+ agent: {
124
+ session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
125
+ resolveAgentWorkspaceDir: () => workspace,
126
+ },
127
+ config: { current: () => ({ agents: { list: [{ id: "main" }] } }) },
128
+ },
129
+ } as any);
130
+
131
+ const res = new MockRes();
132
+ await handleAgentsList(makeReq(AUTH), res as any);
133
+
134
+ const body = JSON.parse(res.body);
135
+ expect(body.agents).toEqual([{ id: "main", name: "星期五 (Friday)", isDefault: true }]);
136
+ } finally {
137
+ fs.rmSync(workspace, { recursive: true, force: true });
138
+ }
139
+ });
140
+
111
141
  it("defaults to the first entry when none is marked default and dedups ids", async () => {
112
142
  setConfig({
113
143
  agents: {
@@ -127,3 +157,24 @@ describe("handleAgentsList", () => {
127
157
  expect(body.agents[0].isDefault).toBe(true);
128
158
  });
129
159
  });
160
+
161
+ describe("parseIdentityNameFromMarkdown", () => {
162
+ it("extracts the Name value from the OpenClaw template format", () => {
163
+ const md = "# IDENTITY.md\n\n- **Name:** 星期五 (Friday)\n- **Emoji:** 🌿\n";
164
+ expect(parseIdentityNameFromMarkdown(md)).toBe("星期五 (Friday)");
165
+ });
166
+
167
+ it("handles a plain unstyled `Name:` line", () => {
168
+ expect(parseIdentityNameFromMarkdown("Name: Jarvis")).toBe("Jarvis");
169
+ });
170
+
171
+ it("returns undefined when there is no Name field", () => {
172
+ expect(parseIdentityNameFromMarkdown("- **Emoji:** 🌿\n- **Vibe:** calm")).toBeUndefined();
173
+ });
174
+
175
+ it("skips the unfilled template placeholder", () => {
176
+ expect(
177
+ parseIdentityNameFromMarkdown("- **Name:** _(pick something you like)_"),
178
+ ).toBeUndefined();
179
+ });
180
+ });
@@ -1,5 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ getFridayAgentForwardRuntime,
6
+ type FridayAgentForwardRuntime,
7
+ } from "../../agent-forward-runtime.js";
3
8
  import { extractBearerToken } from "../middleware/auth.js";
4
9
 
5
10
  const DEFAULT_AGENT_ID = "main";
@@ -54,6 +59,60 @@ function readString(value: unknown): string | undefined {
54
59
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
55
60
  }
56
61
 
62
+ /** Unfilled IDENTITY.md template prompts that must not surface as a real name. */
63
+ const IDENTITY_NAME_PLACEHOLDERS = new Set(["pick something you like"]);
64
+
65
+ /**
66
+ * Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
67
+ * `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
68
+ * drop the leading "- ", split on the first ":", strip markdown emphasis, and
69
+ * skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
70
+ * "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
71
+ */
72
+ export function parseIdentityNameFromMarkdown(content: string): string | undefined {
73
+ for (const rawLine of content.split(/\r?\n/)) {
74
+ const cleaned = rawLine.trim().replace(/^\s*-\s*/, "");
75
+ const colonIndex = cleaned.indexOf(":");
76
+ if (colonIndex === -1) continue;
77
+ const label = cleaned.slice(0, colonIndex).replace(/[*_`]/g, "").trim().toLowerCase();
78
+ if (label !== "name") continue;
79
+ const value = cleaned
80
+ .slice(colonIndex + 1)
81
+ .replace(/^[*_`\s]+|[*_`\s]+$/g, "")
82
+ .trim();
83
+ if (!value) continue;
84
+ let normalized = value.replace(/[–—]/g, "-");
85
+ if (normalized.startsWith("(") && normalized.endsWith(")")) {
86
+ normalized = normalized.slice(1, -1).trim();
87
+ }
88
+ if (IDENTITY_NAME_PLACEHOLDERS.has(normalized.toLowerCase())) continue;
89
+ return value;
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /**
95
+ * Name fallback for agents with no `name`/`identity.name` in config (e.g. the
96
+ * implicit `main`): resolve the agent's workspace and parse its IDENTITY.md, the
97
+ * same source ControlUI reads. Best-effort — any failure yields undefined.
98
+ */
99
+ function readWorkspaceIdentityName(
100
+ rt: FridayAgentForwardRuntime,
101
+ cfg: unknown,
102
+ agentId: string,
103
+ ): string | undefined {
104
+ const resolveWorkspace = rt.resolveAgentWorkspaceDir;
105
+ if (!resolveWorkspace) return undefined;
106
+ try {
107
+ const workspace = resolveWorkspace(cfg, agentId);
108
+ if (!workspace) return undefined;
109
+ const content = fs.readFileSync(path.join(workspace, "IDENTITY.md"), "utf-8");
110
+ return parseIdentityNameFromMarkdown(content);
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
57
116
  /**
58
117
  * Reads the configured agents directly from the runtime config (same approach as
59
118
  * models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
@@ -89,7 +148,10 @@ function resolveConfiguredAgents(): ResolvedAgents {
89
148
  const identity = agent.identity as Record<string, unknown> | undefined;
90
149
  entries.push({
91
150
  id,
92
- name: readString(agent.name) ?? readString(identity?.name),
151
+ name:
152
+ readString(agent.name) ??
153
+ readString(identity?.name) ??
154
+ readWorkspaceIdentityName(rt, cfg, id),
93
155
  description: readString(agent.description),
94
156
  model: resolvePrimaryModel(agent.model),
95
157
  thinkingDefault: readString(agent.thinkingDefault),