@m1heng-clawd/feishu 0.1.7 → 0.1.9

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/src/client.ts CHANGED
@@ -1,66 +1,114 @@
1
1
  import * as Lark from "@larksuiteoapi/node-sdk";
2
- import type { FeishuConfig, FeishuDomain } from "./types.js";
3
- import { resolveFeishuCredentials } from "./accounts.js";
2
+ import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
4
3
 
5
- let cachedClient: Lark.Client | null = null;
6
- let cachedConfig: { appId: string; appSecret: string; domain: FeishuDomain } | null = null;
4
+ // Multi-account client cache
5
+ const clientCache = new Map<
6
+ string,
7
+ {
8
+ client: Lark.Client;
9
+ config: { appId: string; appSecret: string; domain?: FeishuDomain };
10
+ }
11
+ >();
7
12
 
8
- function resolveDomain(domain: FeishuDomain) {
9
- return domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
13
+ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
14
+ if (domain === "lark") return Lark.Domain.Lark;
15
+ if (domain === "feishu" || !domain) return Lark.Domain.Feishu;
16
+ return domain.replace(/\/+$/, ""); // Custom URL for private deployment
10
17
  }
11
18
 
12
- export function createFeishuClient(cfg: FeishuConfig): Lark.Client {
13
- const creds = resolveFeishuCredentials(cfg);
14
- if (!creds) {
15
- throw new Error("Feishu credentials not configured (appId, appSecret required)");
19
+ /**
20
+ * Credentials needed to create a Feishu client.
21
+ * Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
22
+ */
23
+ export type FeishuClientCredentials = {
24
+ accountId?: string;
25
+ appId?: string;
26
+ appSecret?: string;
27
+ domain?: FeishuDomain;
28
+ };
29
+
30
+ /**
31
+ * Create or get a cached Feishu client for an account.
32
+ * Accepts any object with appId, appSecret, and optional domain/accountId.
33
+ */
34
+ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client {
35
+ const { accountId = "default", appId, appSecret, domain } = creds;
36
+
37
+ if (!appId || !appSecret) {
38
+ throw new Error(`Feishu credentials not configured for account "${accountId}"`);
16
39
  }
17
40
 
41
+ // Check cache
42
+ const cached = clientCache.get(accountId);
18
43
  if (
19
- cachedClient &&
20
- cachedConfig &&
21
- cachedConfig.appId === creds.appId &&
22
- cachedConfig.appSecret === creds.appSecret &&
23
- cachedConfig.domain === creds.domain
44
+ cached &&
45
+ cached.config.appId === appId &&
46
+ cached.config.appSecret === appSecret &&
47
+ cached.config.domain === domain
24
48
  ) {
25
- return cachedClient;
49
+ return cached.client;
26
50
  }
27
51
 
52
+ // Create new client
28
53
  const client = new Lark.Client({
29
- appId: creds.appId,
30
- appSecret: creds.appSecret,
54
+ appId,
55
+ appSecret,
31
56
  appType: Lark.AppType.SelfBuild,
32
- domain: resolveDomain(creds.domain),
57
+ domain: resolveDomain(domain),
33
58
  });
34
59
 
35
- cachedClient = client;
36
- cachedConfig = { appId: creds.appId, appSecret: creds.appSecret, domain: creds.domain };
60
+ // Cache it
61
+ clientCache.set(accountId, {
62
+ client,
63
+ config: { appId, appSecret, domain },
64
+ });
37
65
 
38
66
  return client;
39
67
  }
40
68
 
41
- export function createFeishuWSClient(cfg: FeishuConfig): Lark.WSClient {
42
- const creds = resolveFeishuCredentials(cfg);
43
- if (!creds) {
44
- throw new Error("Feishu credentials not configured (appId, appSecret required)");
69
+ /**
70
+ * Create a Feishu WebSocket client for an account.
71
+ * Note: WSClient is not cached since each call creates a new connection.
72
+ */
73
+ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSClient {
74
+ const { accountId, appId, appSecret, domain } = account;
75
+
76
+ if (!appId || !appSecret) {
77
+ throw new Error(`Feishu credentials not configured for account "${accountId}"`);
45
78
  }
46
79
 
47
80
  return new Lark.WSClient({
48
- appId: creds.appId,
49
- appSecret: creds.appSecret,
50
- domain: resolveDomain(creds.domain),
81
+ appId,
82
+ appSecret,
83
+ domain: resolveDomain(domain),
51
84
  loggerLevel: Lark.LoggerLevel.info,
52
85
  });
53
86
  }
54
87
 
55
- export function createEventDispatcher(cfg: FeishuConfig): Lark.EventDispatcher {
56
- const creds = resolveFeishuCredentials(cfg);
88
+ /**
89
+ * Create an event dispatcher for an account.
90
+ */
91
+ export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
57
92
  return new Lark.EventDispatcher({
58
- encryptKey: creds?.encryptKey,
59
- verificationToken: creds?.verificationToken,
93
+ encryptKey: account.encryptKey,
94
+ verificationToken: account.verificationToken,
60
95
  });
61
96
  }
62
97
 
63
- export function clearClientCache() {
64
- cachedClient = null;
65
- cachedConfig = null;
98
+ /**
99
+ * Get a cached client for an account (if exists).
100
+ */
101
+ export function getFeishuClient(accountId: string): Lark.Client | null {
102
+ return clientCache.get(accountId)?.client ?? null;
103
+ }
104
+
105
+ /**
106
+ * Clear client cache for a specific account or all accounts.
107
+ */
108
+ export function clearClientCache(accountId?: string): void {
109
+ if (accountId) {
110
+ clientCache.delete(accountId);
111
+ } else {
112
+ clientCache.clear();
113
+ }
66
114
  }
@@ -3,7 +3,10 @@ export { z };
3
3
 
4
4
  const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
5
5
  const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
6
- const FeishuDomainSchema = z.enum(["feishu", "lark"]);
6
+ const FeishuDomainSchema = z.union([
7
+ z.enum(["feishu", "lark"]),
8
+ z.string().url().startsWith("https://"),
9
+ ]);
7
10
  const FeishuConnectionModeSchema = z.enum(["websocket", "webhook"]);
8
11
 
9
12
  const ToolPolicySchema = z
@@ -50,6 +53,20 @@ const ChannelHeartbeatVisibilitySchema = z
50
53
  .strict()
51
54
  .optional();
52
55
 
56
+ /**
57
+ * Dynamic agent creation configuration.
58
+ * When enabled, a new agent is created for each unique DM user.
59
+ */
60
+ const DynamicAgentCreationSchema = z
61
+ .object({
62
+ enabled: z.boolean().optional(),
63
+ workspaceTemplate: z.string().optional(),
64
+ agentDirTemplate: z.string().optional(),
65
+ maxAgents: z.number().int().positive().optional(),
66
+ })
67
+ .strict()
68
+ .optional();
69
+
53
70
  /**
54
71
  * Feishu tools configuration.
55
72
  * Controls which tool categories are enabled.
@@ -69,6 +86,16 @@ const FeishuToolsConfigSchema = z
69
86
  .strict()
70
87
  .optional();
71
88
 
89
+ /**
90
+ * Topic session isolation mode for group chats.
91
+ * - "disabled" (default): All messages in a group share one session
92
+ * - "enabled": Messages in different topics get separate sessions
93
+ *
94
+ * When enabled, the session key becomes `chat:{chatId}:topic:{rootId}`
95
+ * for messages within a topic thread, allowing isolated conversations.
96
+ */
97
+ const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
98
+
72
99
  export const FeishuGroupSchema = z
73
100
  .object({
74
101
  requireMention: z.boolean().optional(),
@@ -77,12 +104,52 @@ export const FeishuGroupSchema = z
77
104
  enabled: z.boolean().optional(),
78
105
  allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
79
106
  systemPrompt: z.string().optional(),
107
+ topicSessionMode: TopicSessionModeSchema,
108
+ })
109
+ .strict();
110
+
111
+ /**
112
+ * Per-account configuration.
113
+ * All fields are optional - missing fields inherit from top-level config.
114
+ */
115
+ export const FeishuAccountConfigSchema = z
116
+ .object({
117
+ enabled: z.boolean().optional(),
118
+ name: z.string().optional(), // Display name for this account
119
+ appId: z.string().optional(),
120
+ appSecret: z.string().optional(),
121
+ encryptKey: z.string().optional(),
122
+ verificationToken: z.string().optional(),
123
+ domain: FeishuDomainSchema.optional(),
124
+ connectionMode: FeishuConnectionModeSchema.optional(),
125
+ webhookPath: z.string().optional(),
126
+ webhookPort: z.number().int().positive().optional(),
127
+ capabilities: z.array(z.string()).optional(),
128
+ markdown: MarkdownConfigSchema,
129
+ configWrites: z.boolean().optional(),
130
+ dmPolicy: DmPolicySchema.optional(),
131
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
132
+ groupPolicy: GroupPolicySchema.optional(),
133
+ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
134
+ requireMention: z.boolean().optional(),
135
+ groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
136
+ historyLimit: z.number().int().min(0).optional(),
137
+ dmHistoryLimit: z.number().int().min(0).optional(),
138
+ dms: z.record(z.string(), DmConfigSchema).optional(),
139
+ textChunkLimit: z.number().int().positive().optional(),
140
+ chunkMode: z.enum(["length", "newline"]).optional(),
141
+ blockStreamingCoalesce: BlockStreamingCoalesceSchema,
142
+ mediaMaxMb: z.number().positive().optional(),
143
+ heartbeat: ChannelHeartbeatVisibilitySchema,
144
+ renderMode: RenderModeSchema,
145
+ tools: FeishuToolsConfigSchema,
80
146
  })
81
147
  .strict();
82
148
 
83
149
  export const FeishuConfigSchema = z
84
150
  .object({
85
151
  enabled: z.boolean().optional(),
152
+ // Top-level credentials (backward compatible for single-account mode)
86
153
  appId: z.string().optional(),
87
154
  appSecret: z.string().optional(),
88
155
  encryptKey: z.string().optional(),
@@ -100,6 +167,7 @@ export const FeishuConfigSchema = z
100
167
  groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
101
168
  requireMention: z.boolean().optional().default(true),
102
169
  groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
170
+ topicSessionMode: TopicSessionModeSchema,
103
171
  historyLimit: z.number().int().min(0).optional(),
104
172
  dmHistoryLimit: z.number().int().min(0).optional(),
105
173
  dms: z.record(z.string(), DmConfigSchema).optional(),
@@ -110,6 +178,10 @@ export const FeishuConfigSchema = z
110
178
  heartbeat: ChannelHeartbeatVisibilitySchema,
111
179
  renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
112
180
  tools: FeishuToolsConfigSchema,
181
+ // Dynamic agent creation for DM users
182
+ dynamicAgentCreation: DynamicAgentCreationSchema,
183
+ // Multi-account configuration
184
+ accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
113
185
  })
114
186
  .strict()
115
187
  .superRefine((value, ctx) => {
package/src/directory.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import type { FeishuConfig } from "./types.js";
3
2
  import { createFeishuClient } from "./client.js";
3
+ import { resolveFeishuAccount } from "./accounts.js";
4
4
  import { normalizeFeishuTarget } from "./targets.js";
5
5
 
6
6
  export type FeishuDirectoryPeer = {
@@ -19,8 +19,10 @@ export async function listFeishuDirectoryPeers(params: {
19
19
  cfg: ClawdbotConfig;
20
20
  query?: string;
21
21
  limit?: number;
22
+ accountId?: string;
22
23
  }): Promise<FeishuDirectoryPeer[]> {
23
- const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
24
+ const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
25
+ const feishuCfg = account.config;
24
26
  const q = params.query?.trim().toLowerCase() || "";
25
27
  const ids = new Set<string>();
26
28
 
@@ -47,8 +49,10 @@ export async function listFeishuDirectoryGroups(params: {
47
49
  cfg: ClawdbotConfig;
48
50
  query?: string;
49
51
  limit?: number;
52
+ accountId?: string;
50
53
  }): Promise<FeishuDirectoryGroup[]> {
51
- const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
54
+ const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
55
+ const feishuCfg = account.config;
52
56
  const q = params.query?.trim().toLowerCase() || "";
53
57
  const ids = new Set<string>();
54
58
 
@@ -74,14 +78,15 @@ export async function listFeishuDirectoryPeersLive(params: {
74
78
  cfg: ClawdbotConfig;
75
79
  query?: string;
76
80
  limit?: number;
81
+ accountId?: string;
77
82
  }): Promise<FeishuDirectoryPeer[]> {
78
- const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
79
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
83
+ const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
84
+ if (!account.configured) {
80
85
  return listFeishuDirectoryPeers(params);
81
86
  }
82
87
 
83
88
  try {
84
- const client = createFeishuClient(feishuCfg);
89
+ const client = createFeishuClient(account);
85
90
  const peers: FeishuDirectoryPeer[] = [];
86
91
  const limit = params.limit ?? 50;
87
92
 
@@ -118,14 +123,15 @@ export async function listFeishuDirectoryGroupsLive(params: {
118
123
  cfg: ClawdbotConfig;
119
124
  query?: string;
120
125
  limit?: number;
126
+ accountId?: string;
121
127
  }): Promise<FeishuDirectoryGroup[]> {
122
- const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
123
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
128
+ const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
129
+ if (!account.configured) {
124
130
  return listFeishuDirectoryGroups(params);
125
131
  }
126
132
 
127
133
  try {
128
- const client = createFeishuClient(feishuCfg);
134
+ const client = createFeishuClient(account);
129
135
  const groups: FeishuDirectoryGroup[] = [];
130
136
  const limit = params.limit ?? 50;
131
137
 
package/src/docx.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  import { createFeishuClient } from "./client.js";
4
- import type { FeishuConfig } from "./types.js";
4
+ import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
5
5
  import type * as Lark from "@larksuiteoapi/node-sdk";
6
6
  import { Readable } from "stream";
7
7
  import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
@@ -388,14 +388,24 @@ async function listAppScopes(client: Lark.Client) {
388
388
  // ============ Tool Registration ============
389
389
 
390
390
  export function registerFeishuDocTools(api: OpenClawPluginApi) {
391
- const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
392
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
393
- api.logger.debug?.("feishu_doc: Feishu credentials not configured, skipping doc tools");
391
+ if (!api.config) {
392
+ api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
394
393
  return;
395
394
  }
396
395
 
397
- const toolsCfg = resolveToolsConfig(feishuCfg.tools);
398
- const getClient = () => createFeishuClient(feishuCfg);
396
+ // Check if any account is configured
397
+ const accounts = listEnabledFeishuAccounts(api.config);
398
+ if (accounts.length === 0) {
399
+ api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
400
+ return;
401
+ }
402
+
403
+ // Use first account's config for tools configuration
404
+ const firstAccount = accounts[0];
405
+ const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
406
+
407
+ // Helper to get client for the default account
408
+ const getClient = () => createFeishuClient(firstAccount);
399
409
  const registered: string[] = [];
400
410
 
401
411
  // Main document tool with action-based dispatch
package/src/drive.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { createFeishuClient } from "./client.js";
3
- import type { FeishuConfig } from "./types.js";
3
+ import { listEnabledFeishuAccounts } from "./accounts.js";
4
4
  import type * as Lark from "@larksuiteoapi/node-sdk";
5
5
  import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
6
6
  import { resolveToolsConfig } from "./tools-config.js";
@@ -150,19 +150,25 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
150
150
  // ============ Tool Registration ============
151
151
 
152
152
  export function registerFeishuDriveTools(api: OpenClawPluginApi) {
153
- const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
154
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
155
- api.logger.debug?.("feishu_drive: Feishu credentials not configured, skipping drive tools");
153
+ if (!api.config) {
154
+ api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
156
155
  return;
157
156
  }
158
157
 
159
- const toolsCfg = resolveToolsConfig(feishuCfg.tools);
158
+ const accounts = listEnabledFeishuAccounts(api.config);
159
+ if (accounts.length === 0) {
160
+ api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
161
+ return;
162
+ }
163
+
164
+ const firstAccount = accounts[0];
165
+ const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
160
166
  if (!toolsCfg.drive) {
161
167
  api.logger.debug?.("feishu_drive: drive tool disabled in config");
162
168
  return;
163
169
  }
164
170
 
165
- const getClient = () => createFeishuClient(feishuCfg);
171
+ const getClient = () => createFeishuClient(firstAccount);
166
172
 
167
173
  api.registerTool(
168
174
  {
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
5
+ import type { DynamicAgentCreationConfig } from "./types.js";
6
+
7
+ export type MaybeCreateDynamicAgentResult = {
8
+ created: boolean;
9
+ updatedCfg: OpenClawConfig;
10
+ agentId?: string;
11
+ };
12
+
13
+ /**
14
+ * Check if a dynamic agent should be created for a DM user and create it if needed.
15
+ * This creates a unique agent instance with its own workspace for each DM user.
16
+ */
17
+ export async function maybeCreateDynamicAgent(params: {
18
+ cfg: OpenClawConfig;
19
+ runtime: PluginRuntime;
20
+ senderOpenId: string;
21
+ dynamicCfg: DynamicAgentCreationConfig;
22
+ log: (msg: string) => void;
23
+ }): Promise<MaybeCreateDynamicAgentResult> {
24
+ const { cfg, runtime, senderOpenId, dynamicCfg, log } = params;
25
+
26
+ // Check if there's already a binding for this user
27
+ const existingBindings = cfg.bindings ?? [];
28
+ const hasBinding = existingBindings.some(
29
+ (b) =>
30
+ b.match?.channel === "feishu" &&
31
+ b.match?.peer?.kind === "dm" &&
32
+ b.match?.peer?.id === senderOpenId,
33
+ );
34
+
35
+ if (hasBinding) {
36
+ return { created: false, updatedCfg: cfg };
37
+ }
38
+
39
+ // Check maxAgents limit if configured
40
+ if (dynamicCfg.maxAgents !== undefined) {
41
+ const feishuAgentCount = (cfg.agents?.list ?? []).filter((a) =>
42
+ a.id.startsWith("feishu-"),
43
+ ).length;
44
+ if (feishuAgentCount >= dynamicCfg.maxAgents) {
45
+ log(
46
+ `feishu: maxAgents limit (${dynamicCfg.maxAgents}) reached, not creating agent for ${senderOpenId}`,
47
+ );
48
+ return { created: false, updatedCfg: cfg };
49
+ }
50
+ }
51
+
52
+ // Use full OpenID as agent ID suffix (OpenID format: ou_xxx is already filesystem-safe)
53
+ const agentId = `feishu-${senderOpenId}`;
54
+
55
+ // Check if agent already exists (but binding was missing)
56
+ const existingAgent = (cfg.agents?.list ?? []).find((a) => a.id === agentId);
57
+ if (existingAgent) {
58
+ // Agent exists but binding doesn't - just add the binding
59
+ log(`feishu: agent "${agentId}" exists, adding missing binding for ${senderOpenId}`);
60
+
61
+ const updatedCfg: OpenClawConfig = {
62
+ ...cfg,
63
+ bindings: [
64
+ ...existingBindings,
65
+ {
66
+ agentId,
67
+ match: {
68
+ channel: "feishu",
69
+ peer: { kind: "dm", id: senderOpenId },
70
+ },
71
+ },
72
+ ],
73
+ };
74
+
75
+ await runtime.config.writeConfigFile(updatedCfg);
76
+ return { created: true, updatedCfg, agentId };
77
+ }
78
+
79
+ // Resolve path templates with substitutions
80
+ const workspaceTemplate = dynamicCfg.workspaceTemplate ?? "~/.openclaw/workspace-{agentId}";
81
+ const agentDirTemplate = dynamicCfg.agentDirTemplate ?? "~/.openclaw/agents/{agentId}/agent";
82
+
83
+ const workspace = resolveUserPath(
84
+ workspaceTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
85
+ );
86
+ const agentDir = resolveUserPath(
87
+ agentDirTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
88
+ );
89
+
90
+ log(`feishu: creating dynamic agent "${agentId}" for user ${senderOpenId}`);
91
+ log(` workspace: ${workspace}`);
92
+ log(` agentDir: ${agentDir}`);
93
+
94
+ // Create directories
95
+ await fs.promises.mkdir(workspace, { recursive: true });
96
+ await fs.promises.mkdir(agentDir, { recursive: true });
97
+
98
+ // Update configuration with new agent and binding
99
+ const updatedCfg: OpenClawConfig = {
100
+ ...cfg,
101
+ agents: {
102
+ ...cfg.agents,
103
+ list: [...(cfg.agents?.list ?? []), { id: agentId, workspace, agentDir }],
104
+ },
105
+ bindings: [
106
+ ...existingBindings,
107
+ {
108
+ agentId,
109
+ match: {
110
+ channel: "feishu",
111
+ peer: { kind: "dm", id: senderOpenId },
112
+ },
113
+ },
114
+ ],
115
+ };
116
+
117
+ // Write updated config using PluginRuntime API
118
+ await runtime.config.writeConfigFile(updatedCfg);
119
+
120
+ return { created: true, updatedCfg, agentId };
121
+ }
122
+
123
+ /**
124
+ * Resolve a path that may start with ~ to the user's home directory.
125
+ */
126
+ function resolveUserPath(p: string): string {
127
+ if (p.startsWith("~/")) {
128
+ return path.join(os.homedir(), p.slice(2));
129
+ }
130
+ return p;
131
+ }