@alfe.ai/openclaw-mobile 0.0.25 → 0.0.27

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/dist/index.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import plugin from "./plugin.cjs";
1
+ import { t as plugin } from "./plugin.cjs";
2
2
  export { plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import plugin from "./plugin.js";
1
+ import { t as plugin } from "./plugin.js";
2
2
  export { plugin as default };
package/dist/plugin.cjs CHANGED
@@ -1,6 +1,8 @@
1
- let _sinclair_typebox = require("@sinclair/typebox");
1
+ let node_module = require("node:module");
2
2
  let _alfe_ai_config = require("@alfe.ai/config");
3
3
  let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
4
+ let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
5
+ let _sinclair_typebox = require("@sinclair/typebox");
4
6
  //#region src/plugin.ts
5
7
  /**
6
8
  * @alfe/openclaw-mobile — OpenClaw native plugin
@@ -16,141 +18,161 @@ let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
16
18
  * - mobile_send_sms — send an SMS message from the agent's phone number
17
19
  * - mobile_call — initiate an outbound phone call
18
20
  */
19
- const pkg = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
20
- function ok(data) {
21
- return {
22
- content: [{
23
- type: "text",
24
- text: JSON.stringify(data)
25
- }],
26
- details: data
27
- };
21
+ const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
22
+ const SUPPORTED_COUNTRIES = [
23
+ "AU",
24
+ "US",
25
+ "CA",
26
+ "GB"
27
+ ];
28
+ const E164_PATTERN = "^\\+\\d{7,15}$";
29
+ const E164_RE = /^\+\d{7,15}$/u;
30
+ const NUMBER_SEARCH_RE = /^[A-Za-z0-9*+ -]+$/u;
31
+ const MAX_SEARCH_LENGTH = 32;
32
+ const MAX_SMS_LENGTH = 4400;
33
+ function requireString(params, field, options) {
34
+ const value = params[field];
35
+ if (typeof value !== "string" || value.length < 1 || value.length > options.maxLength) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${field} must be a non-empty string of at most ${String(options.maxLength)} characters`);
36
+ if (options.pattern && !options.pattern.test(value)) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${field} has an invalid format`);
37
+ return value;
28
38
  }
29
- function errResult(message) {
30
- return {
31
- content: [{
32
- type: "text",
33
- text: JSON.stringify({ error: message })
34
- }],
35
- details: { error: message }
36
- };
39
+ function optionalSearchQuery(params) {
40
+ if (params.query === void 0) return void 0;
41
+ return requireString(params, "query", {
42
+ maxLength: MAX_SEARCH_LENGTH,
43
+ pattern: NUMBER_SEARCH_RE
44
+ });
37
45
  }
38
- function defineTool(def) {
39
- return {
40
- name: def.name,
41
- description: def.description,
42
- label: def.name,
43
- parameters: def.parameters,
44
- execute: async (_toolCallId, params) => {
45
- try {
46
- return ok(await def.handler(params));
47
- } catch (e) {
48
- return errResult(e.message);
49
- }
50
- }
51
- };
46
+ function optionalCountry(params) {
47
+ if (params.country === void 0) return void 0;
48
+ return requireCountry(params, "country");
52
49
  }
53
- let apiUrl = "";
54
- let apiKey = "";
55
- async function mobileApi(method, path, body, query) {
56
- let url = `${apiUrl}${path}`;
57
- if (query) {
58
- const entries = Object.entries(query).filter((entry) => entry[1] !== void 0);
59
- if (entries.length > 0) url += `?${new URLSearchParams(entries).toString()}`;
60
- }
61
- const headers = {
62
- "Content-Type": "application/json",
63
- Authorization: `Bearer ${apiKey}`
64
- };
65
- const res = await fetch(url, {
66
- method,
67
- headers,
68
- body: body ? JSON.stringify(body) : void 0
50
+ function requireCountry(params, field) {
51
+ const value = params[field];
52
+ if (typeof value !== "string" || !SUPPORTED_COUNTRIES.includes(value)) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(`${field} must be one of ${SUPPORTED_COUNTRIES.join(", ")}`);
53
+ return value;
54
+ }
55
+ function requireE164(params, field) {
56
+ return requireString(params, field, {
57
+ maxLength: 16,
58
+ pattern: E164_RE
69
59
  });
70
- const json = await res.json();
71
- if (!res.ok) {
72
- const issues = json.issues;
73
- if (Array.isArray(issues) && issues.length > 0) {
74
- const details = issues.map((i) => {
75
- return `${i.path?.join(".") ?? "input"}: ${i.message ?? "invalid"}`;
76
- }).join("; ");
77
- throw new Error(`Validation error${details}`);
78
- }
79
- const errorMsg = typeof json.error === "string" ? json.error : typeof json.message === "string" ? json.message : `Mobile service returned ${String(res.status)}`;
80
- throw new Error(errorMsg);
81
- }
82
- if (json.data && typeof json.data === "object") return json.data;
83
- return json;
60
+ }
61
+ function requireConfirmation(params) {
62
+ if (params.confirm !== true) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("confirm must be true after the user explicitly approves the recurring charge");
63
+ }
64
+ const MOBILE_ACTIVATION_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("mobile");
65
+ let client;
66
+ function getClient() {
67
+ if (!client) throw new Error("Mobile tools unavailable no API config resolved");
68
+ return client;
84
69
  }
85
70
  const mobileTools = [
86
- defineTool({
71
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
87
72
  name: "mobile_get_number",
88
73
  description: "Check if you have a phone number assigned. Returns the phone number, country code, monthly price, and status. If no number is assigned, use mobile_search_numbers and mobile_assign_number to get one.",
89
- parameters: _sinclair_typebox.Type.Object({}),
74
+ parameters: _sinclair_typebox.Type.Object({}, { additionalProperties: false }),
90
75
  handler: async () => {
91
- return mobileApi("GET", "/mobile/numbers");
76
+ return getClient().getMobileNumber();
92
77
  }
93
78
  }),
94
- defineTool({
79
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
95
80
  name: "mobile_search_numbers",
96
81
  description: "Search for available phone numbers that can be purchased. Returns a list of available numbers and the monthly price. Supported countries: AU (Australia), US (United States), CA (Canada), GB (United Kingdom).",
97
82
  parameters: _sinclair_typebox.Type.Object({
98
- country: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Country code: AU, US, CA, or GB (default: AU)" })),
99
- query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Optional search pattern — area code or text to match (e.g., '415' or 'COOL')" }))
100
- }),
83
+ country: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union(SUPPORTED_COUNTRIES.map((country) => _sinclair_typebox.Type.Literal(country)), { description: "Country code: AU, US, CA, or GB (default: AU)" })),
84
+ query: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
85
+ description: "Optional search pattern — area code or text to match (e.g., '415' or 'COOL')",
86
+ minLength: 1,
87
+ maxLength: MAX_SEARCH_LENGTH,
88
+ pattern: NUMBER_SEARCH_RE.source
89
+ }))
90
+ }, { additionalProperties: false }),
101
91
  handler: async (params) => {
102
- const { country, query } = params;
103
- return mobileApi("GET", "/mobile/numbers/search", void 0, {
92
+ const country = optionalCountry(params);
93
+ const query = optionalSearchQuery(params);
94
+ return getClient().searchMobileNumbers({
104
95
  country,
105
96
  query
106
97
  });
107
98
  }
108
99
  }),
109
- defineTool({
100
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
110
101
  name: "mobile_assign_number",
111
- description: "Purchase and assign a phone number to yourself. You must search for available numbers first using mobile_search_numbers, then pass the chosen phoneNumber and countryCode here. Your organisation must have a payment method on file. The number will be billed monthly to the organisation's account.",
102
+ description: "Purchase and assign a phone number to yourself. You must search for available numbers first using mobile_search_numbers, then pass the chosen phoneNumber and countryCode here. Your organisation must have a payment method on file. The number will be billed monthly to the organisation's account and may replace an active number. Set confirm=true only after the user explicitly approves the charge.",
112
103
  parameters: _sinclair_typebox.Type.Object({
113
- phoneNumber: _sinclair_typebox.Type.String({ description: "The phone number to purchase (from mobile_search_numbers results)" }),
114
- countryCode: _sinclair_typebox.Type.String({ description: "Country code: AU, US, CA, or GB" })
115
- }),
104
+ phoneNumber: _sinclair_typebox.Type.String({
105
+ description: "The phone number to purchase (from mobile_search_numbers results)",
106
+ minLength: 8,
107
+ maxLength: 16,
108
+ pattern: E164_PATTERN
109
+ }),
110
+ countryCode: _sinclair_typebox.Type.Union(SUPPORTED_COUNTRIES.map((country) => _sinclair_typebox.Type.Literal(country)), { description: "Country code: AU, US, CA, or GB" }),
111
+ confirm: _sinclair_typebox.Type.Literal(true, { description: "Must be true after the user explicitly approves the recurring charge" })
112
+ }, { additionalProperties: false }),
116
113
  handler: async (params) => {
117
- const { phoneNumber, countryCode } = params;
118
- return mobileApi("POST", "/mobile/numbers/assign", {
114
+ const phoneNumber = requireE164(params, "phoneNumber");
115
+ const countryCode = requireCountry(params, "countryCode");
116
+ requireConfirmation(params);
117
+ return getClient().assignMobileNumber({
119
118
  phoneNumber,
120
119
  countryCode
121
120
  });
122
121
  }
123
122
  }),
124
- defineTool({
123
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
125
124
  name: "mobile_release_number",
126
- description: "Release your currently assigned phone number. This will cancel the monthly subscription and the number will no longer be available for calls or SMS. This action cannot be undone — the same number may not be available again.",
127
- parameters: _sinclair_typebox.Type.Object({}),
128
- handler: async () => {
129
- return mobileApi("POST", "/mobile/numbers/release", {});
125
+ description: "Release your currently assigned phone number. This will cancel the monthly subscription and the number will no longer be available for calls or SMS. This action cannot be undone — the same number may not be available again. Copy the exact current number into confirmPhoneNumber only after the user approves.",
126
+ parameters: _sinclair_typebox.Type.Object({ confirmPhoneNumber: _sinclair_typebox.Type.String({
127
+ description: "Exact currently assigned number, copied to confirm permanent release",
128
+ minLength: 8,
129
+ maxLength: 16,
130
+ pattern: E164_PATTERN
131
+ }) }, { additionalProperties: false }),
132
+ handler: async (params) => {
133
+ const confirmPhoneNumber = requireE164(params, "confirmPhoneNumber");
134
+ const mobileClient = getClient();
135
+ if ((await mobileClient.getMobileNumber()).phoneNumber !== confirmPhoneNumber) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("confirmPhoneNumber does not match the currently assigned number");
136
+ return mobileClient.releaseMobileNumber();
130
137
  }
131
138
  }),
132
- defineTool({
139
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
133
140
  name: "mobile_send_sms",
134
141
  description: "Send an SMS text message from your phone number to the specified number. The recipient number must be in E.164 format (e.g., +12025551234).",
135
142
  parameters: _sinclair_typebox.Type.Object({
136
- to: _sinclair_typebox.Type.String({ description: "Recipient phone number in E.164 format (e.g., +12025551234)" }),
137
- body: _sinclair_typebox.Type.String({ description: "The text message content to send" })
138
- }),
143
+ to: _sinclair_typebox.Type.String({
144
+ description: "Recipient phone number in E.164 format (e.g., +12025551234)",
145
+ minLength: 8,
146
+ maxLength: 16,
147
+ pattern: E164_PATTERN
148
+ }),
149
+ body: _sinclair_typebox.Type.String({
150
+ description: "The text message content to send",
151
+ minLength: 1,
152
+ maxLength: MAX_SMS_LENGTH
153
+ })
154
+ }, { additionalProperties: false }),
139
155
  handler: async (params) => {
140
- const { to, body } = params;
141
- return mobileApi("POST", "/mobile/sms/send", {
156
+ const to = requireE164(params, "to");
157
+ const body = requireString(params, "body", { maxLength: MAX_SMS_LENGTH });
158
+ return getClient().sendSms({
142
159
  to,
143
160
  body
144
161
  });
145
162
  }
146
163
  }),
147
- defineTool({
164
+ (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
148
165
  name: "mobile_call",
149
166
  description: "Make an outbound phone call to the specified number. When the recipient answers, they will be connected to your voice pipeline. The number must be in E.164 format (e.g., +12025551234).",
150
- parameters: _sinclair_typebox.Type.Object({ to: _sinclair_typebox.Type.String({ description: "Phone number to call in E.164 format (e.g., +12025551234)" }) }),
167
+ parameters: _sinclair_typebox.Type.Object({ to: _sinclair_typebox.Type.String({
168
+ description: "Phone number to call in E.164 format (e.g., +12025551234)",
169
+ minLength: 8,
170
+ maxLength: 16,
171
+ pattern: E164_PATTERN
172
+ }) }, { additionalProperties: false }),
151
173
  handler: async (params) => {
152
- const { to } = params;
153
- return mobileApi("POST", "/mobile/calls/outbound", { to });
174
+ const to = requireE164(params, "to");
175
+ return getClient().startOutboundCall({ to });
154
176
  }
155
177
  })
156
178
  ];
@@ -163,24 +185,27 @@ const plugin = {
163
185
  (0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-mobile" });
164
186
  const log = api.logger;
165
187
  for (const tool of mobileTools) api.registerTool(tool);
166
- log.info(`Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((t) => t.name).join(", ")}`);
167
- if (!globalThis.__mobilePluginActivated) {
168
- globalThis.__mobilePluginActivated = true;
188
+ log.info(`Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((tool) => tool.name).join(", ")}`);
189
+ (0, _alfe_ai_openclaw_plugin_kit.guardedStart)(MOBILE_ACTIVATION_KEY, log, () => {
169
190
  log.info("Alfe Mobile plugin activating...");
170
191
  try {
171
192
  const config = (0, _alfe_ai_config.resolveConfig)();
172
- apiUrl = config.apiUrl;
173
- apiKey = config.apiKey;
193
+ client = new _alfe_ai_agent_api_client.AgentApiClient({
194
+ apiUrl: config.apiUrl,
195
+ apiKey: config.apiKey
196
+ });
197
+ log.info("Mobile API client configured");
174
198
  } catch (err) {
175
199
  log.error(`Failed to resolve config: ${err instanceof Error ? err.message : String(err)}`);
176
200
  log.warn("Mobile tools will fail — no API config available");
201
+ (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(MOBILE_ACTIVATION_KEY);
177
202
  }
178
- log.info(`Mobile API: ${apiUrl}`);
179
- }
203
+ });
180
204
  log.info("Alfe Mobile plugin activated");
181
205
  },
182
206
  deactivate(api) {
183
- globalThis.__mobilePluginActivated = false;
207
+ client = void 0;
208
+ (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(MOBILE_ACTIVATION_KEY);
184
209
  api.logger.info("Alfe Mobile plugin deactivated");
185
210
  }
186
211
  };
package/dist/plugin.d.cts CHANGED
@@ -1,5 +1,28 @@
1
1
  import { TSchema } from "@sinclair/typebox";
2
2
 
3
+ //#region ../openclaw-plugin-kit/dist/index.d.ts
4
+
5
+ //# sourceMappingURL=types.d.ts.map
6
+ //#endregion
7
+ //#region src/tools.d.ts
8
+ /** Shape returned to OpenClaw from a tool `execute`. */
9
+ interface ToolResult {
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ details: unknown;
15
+ isError?: boolean;
16
+ }
17
+ interface ToolDef<TParameters = unknown> {
18
+ name: string;
19
+ description: string;
20
+ label: string;
21
+ parameters: TParameters;
22
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
23
+ }
24
+ /** Deliberately model-safe validation/usage failure. Other exceptions stay private. */
25
+ //#endregion
3
26
  //#region src/plugin.d.ts
4
27
 
5
28
  interface Logger {
@@ -11,25 +34,12 @@ interface Logger {
11
34
  interface OpenClawPluginApi {
12
35
  logger: Logger;
13
36
  registrationMode?: "full" | "setup-only" | "setup-runtime" | "cli-metadata";
14
- registerTool(tool: ToolDef): void;
37
+ registerTool(tool: ToolDef<TSchema>): void;
15
38
  registerGatewayMethod(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
16
39
  on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
17
40
  priority?: number;
18
41
  }): void;
19
42
  }
20
- interface ToolDef {
21
- name: string;
22
- description: string;
23
- label: string;
24
- parameters: TSchema;
25
- execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
26
- content: {
27
- type: "text";
28
- text: string;
29
- }[];
30
- details: unknown;
31
- }>;
32
- }
33
43
  declare const plugin: {
34
44
  id: string;
35
45
  name: string;
@@ -39,5 +49,5 @@ declare const plugin: {
39
49
  deactivate(api: OpenClawPluginApi): void;
40
50
  };
41
51
  //#endregion
42
- export { plugin as default };
52
+ export { plugin as t };
43
53
  //# sourceMappingURL=plugin.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;UAsBU,MAAA,CAsB0D;EAAO,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EA8LrE,IAAA,CAAA,GAAA,EA4CL,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OAtCe,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAnNN,iBAAA,CAqPyB;UApPzB;;qBAEW;uEACkD;4DACX;;;;UAKlD,OAAA;;;;cAII;wCAC0B,4BAA4B;;;;;;;;cA8L9D;;;;;gBAMU;kBAkCE"}
1
+ {"version":3,"file":"plugin.d.cts","names":["KitLogger","ToolResult","ToolDef","TParameters","Record","Promise","PublicToolError","Error","publicToolError","ok","errResult","defineTool","IpcResponse","IpcClient","IpcModule","ConnectToDaemonOptions","ConnectToDaemonDependencies","connectToDaemon","ResolveOpenClawSdkOptions","ResolveOpenClawSdkDependencies","resolveOpenClawSdk","T","getActivationKey","isActivated","resetActivation","guardedStart"],"sources":["../../openclaw-plugin-kit/dist/index.d.ts","../src/plugin.ts"],"sourcesContent":["//#region src/types.d.ts\n/**\n * Minimal logger contract shared by every helper in the kit.\n *\n * Deliberately the *narrowest* shape in the fleet: some plugins declare\n * variadic loggers (`info(msg: string, ...args: unknown[])`), others\n * single-arg (`info(msg: string)`). Both are assignable to this. The kit\n * only ever calls with a single string.\n */\ninterface KitLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n//# sourceMappingURL=types.d.ts.map\n//#endregion\n//#region src/tools.d.ts\n/** Shape returned to OpenClaw from a tool `execute`. */\ninterface ToolResult {\n content: {\n type: \"text\";\n text: string;\n }[];\n details: unknown;\n isError?: boolean;\n}\ninterface ToolDef<TParameters = unknown> {\n name: string;\n description: string;\n label: string;\n parameters: TParameters;\n execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;\n}\n/** Deliberately model-safe validation/usage failure. Other exceptions stay private. */\ndeclare class PublicToolError extends Error {\n readonly name = \"PublicToolError\";\n}\ndeclare function publicToolError(message: string): PublicToolError;\n/** Wrap bounded JSON in the OpenClaw tool-result envelope. */\ndeclare function ok(data: unknown): ToolResult;\n/** Wrap an explicitly public error in a captured OpenClaw error envelope. */\ndeclare function errResult(message: string): ToolResult;\n/**\n * Define a tool whose unexpected exceptions never expose provider, filesystem,\n * credential, or transport diagnostics to the model. Throw `PublicToolError`\n * only for a message deliberately safe for model display.\n */\ndeclare function defineTool<TParameters>(def: {\n name: string;\n description: string;\n parameters: TParameters;\n handler: (params: Record<string, unknown>) => Promise<unknown>;\n}): ToolDef<TParameters>;\n//# sourceMappingURL=tools.d.ts.map\n//#endregion\n//#region src/daemon.d.ts\ninterface IpcResponse {\n ok: boolean;\n error?: {\n message?: string;\n };\n [key: string]: unknown;\n}\ninterface IpcClient {\n on(event: string, handler: (...args: unknown[]) => void): void;\n request(method: string, params: Record<string, unknown>): Promise<IpcResponse>;\n start(): void;\n stop(): void;\n}\ninterface IpcModule {\n IPCClient: new (socketPath: string, log: KitLogger) => IpcClient;\n}\ninterface ConnectToDaemonOptions {\n pluginId: string;\n capabilities?: readonly string[];\n onMessage?: (msg: Record<string, unknown>) => void;\n standaloneNote?: string;\n}\ninterface ConnectToDaemonDependencies {\n loadIpcModule?: () => Promise<IpcModule>;\n}\ndeclare function connectToDaemon(socketPath: string, log: KitLogger, options: ConnectToDaemonOptions, dependencies?: ConnectToDaemonDependencies): Promise<IpcClient | null>;\n//#endregion\n//#region src/sdk.d.ts\ninterface ResolveOpenClawSdkOptions {\n specifier?: string;\n exportName?: string;\n unresolvableNote?: string;\n}\ninterface ResolveOpenClawSdkDependencies {\n anchors?: readonly string[];\n globalPackageJsonPath?: string;\n requireFrom?: (anchor: string) => (specifier: string) => unknown;\n}\ndeclare function resolveOpenClawSdk<T = unknown>(log: KitLogger, options?: ResolveOpenClawSdkOptions, dependencies?: ResolveOpenClawSdkDependencies): T | null;\n//# sourceMappingURL=sdk.d.ts.map\n\n//#endregion\n//#region src/activation.d.ts\n/**\n * Canonical activation-flag key for a plugin: `__alfe<Name>PluginActivated`.\n *\n * Accepts a short name (`\"google\"`, `\"google-chat\"`) or a full package name\n * (`\"@alfe.ai/openclaw-teams\"`); scope + `openclaw-` prefix are stripped\n * and the remainder PascalCased:\n *\n * getActivationKey(\"google\") → \"__alfeGooglePluginActivated\"\n * getActivationKey(\"google-chat\") → \"__alfeGoogleChatPluginActivated\"\n * getActivationKey(\"@alfe.ai/openclaw-teams\") → \"__alfeTeamsPluginActivated\"\n */\ndeclare function getActivationKey(name: string): string;\n/** True when the activation flag for `key` is currently set. */\ndeclare function isActivated(key: string): boolean;\n/**\n * Clear the activation flag so a later start can run again.\n *\n * Call this LAST in stop/deactivate paths — after side effects are stopped\n * (see the ordering rule in the module doc). Also exported for soft-failure\n * paths inside a `guardedStart` fn that want to log at a custom level and\n * return normally instead of throwing.\n */\ndeclare function resetActivation(key: string): void;\n/**\n * Run a service start exactly once per activation cycle.\n *\n * - If the flag is already set, logs at debug and returns `false` (skipped).\n * - Otherwise sets the flag and runs `fn`.\n * - If `fn` throws synchronously OR returns a promise that rejects, the\n * flag is RESET (so a later activate can retry) and the error is logged.\n * Errors are not rethrown — matching fleet behavior where a failed start\n * must never crash the host's plugin loader.\n *\n * Returns `true` when the start was initiated (even if an async portion\n * later fails), `false` when skipped or when `fn` threw synchronously.\n *\n * Note: cleaning up partial side effects on failure is `fn`'s job (throw\n * only after tearing down what was started); the kit only guarantees the\n * flag reset happens after `fn` has failed — i.e. after `fn`'s own cleanup.\n */\ndeclare function guardedStart(key: string, log: KitLogger, fn: () => void | Promise<void>): boolean;\n//# sourceMappingURL=activation.d.ts.map\n\n//#endregion\nexport { type ConnectToDaemonDependencies, type ConnectToDaemonOptions, type IpcClient, type IpcResponse, type KitLogger, PublicToolError, type ResolveOpenClawSdkDependencies, type ResolveOpenClawSdkOptions, type ToolDef, type ToolResult, connectToDaemon, defineTool, errResult, getActivationKey, guardedStart, isActivated, ok, publicToolError, resetActivation, resolveOpenClawSdk };\n//# sourceMappingURL=index.d.ts.map"],"mappings":";;;;ACwCgB;;;;UDrBNC,UAAAA,CC+BW;SACkD,EAAA;QAG3B,EAAA,MAAA;IAAO,IAAA,EAAA,MAAA;EAAA,CAAA,EA4O7C;EA8CL,OAAA,EAAA,OAAA;SAxCe,CAAA,EAAA,OAAA;;UD7QNC,OC+SyB,CAAA,cAAA,OAAA,CAAA,CAAA;;;;cD3SrBC;wCAC0BC,4BAA4BC,QAAQJ;;;;;;UCQlE,MAAA,CAOiB;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MAEmB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OAAR,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACkD,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAJ7D,iBAAA,CAOyC;EA4O7C,MAAA,EAlPI,MAgST;EAAA,gBAAA,CAAA,EAAA,MAAA,GAAA,YAAA,GAAA,eAAA,GAAA,cAAA;cAxCe,CAAA,IAAA,EAtPK,OAsPL,CAtPa,OAsPb,CAAA,CAAA,EAAA,IAAA;uBAkCE,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,GAAA,IAAA,EAAA,OAAA,EAAA,EAAA,GAvRqD,OAuRrD,CAAA,OAAA,CAAA,CAAA,EAAA,IAAA;EAAiB,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,GAAA,IAAA,EAAA,OAAA,EAAA,EAAA,GAAA,IAAA,GApRS,OAoRT,CAAA,IAAA,CAAA,EAAA,QAAA,EAAA;;;;cAxC7B;;;;;gBAMU;kBAkCE"}
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,28 @@
1
1
  import { TSchema } from "@sinclair/typebox";
2
2
 
3
+ //#region ../openclaw-plugin-kit/dist/index.d.ts
4
+
5
+ //# sourceMappingURL=types.d.ts.map
6
+ //#endregion
7
+ //#region src/tools.d.ts
8
+ /** Shape returned to OpenClaw from a tool `execute`. */
9
+ interface ToolResult {
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ details: unknown;
15
+ isError?: boolean;
16
+ }
17
+ interface ToolDef<TParameters = unknown> {
18
+ name: string;
19
+ description: string;
20
+ label: string;
21
+ parameters: TParameters;
22
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
23
+ }
24
+ /** Deliberately model-safe validation/usage failure. Other exceptions stay private. */
25
+ //#endregion
3
26
  //#region src/plugin.d.ts
4
27
 
5
28
  interface Logger {
@@ -11,25 +34,12 @@ interface Logger {
11
34
  interface OpenClawPluginApi {
12
35
  logger: Logger;
13
36
  registrationMode?: "full" | "setup-only" | "setup-runtime" | "cli-metadata";
14
- registerTool(tool: ToolDef): void;
37
+ registerTool(tool: ToolDef<TSchema>): void;
15
38
  registerGatewayMethod(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
16
39
  on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
17
40
  priority?: number;
18
41
  }): void;
19
42
  }
20
- interface ToolDef {
21
- name: string;
22
- description: string;
23
- label: string;
24
- parameters: TSchema;
25
- execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
26
- content: {
27
- type: "text";
28
- text: string;
29
- }[];
30
- details: unknown;
31
- }>;
32
- }
33
43
  declare const plugin: {
34
44
  id: string;
35
45
  name: string;
@@ -39,5 +49,5 @@ declare const plugin: {
39
49
  deactivate(api: OpenClawPluginApi): void;
40
50
  };
41
51
  //#endregion
42
- export { plugin as default };
52
+ export { plugin as t };
43
53
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;UAsBU,MAAA,CAsB0D;EAAO,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EA8LrE,IAAA,CAAA,GAAA,EA4CL,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OAtCe,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAnNN,iBAAA,CAqPyB;UApPzB;;qBAEW;uEACkD;4DACX;;;;UAKlD,OAAA;;;;cAII;wCAC0B,4BAA4B;;;;;;;;cA8L9D;;;;;gBAMU;kBAkCE"}
1
+ {"version":3,"file":"plugin.d.ts","names":["KitLogger","ToolResult","ToolDef","TParameters","Record","Promise","PublicToolError","Error","publicToolError","ok","errResult","defineTool","IpcResponse","IpcClient","IpcModule","ConnectToDaemonOptions","ConnectToDaemonDependencies","connectToDaemon","ResolveOpenClawSdkOptions","ResolveOpenClawSdkDependencies","resolveOpenClawSdk","T","getActivationKey","isActivated","resetActivation","guardedStart"],"sources":["../../openclaw-plugin-kit/dist/index.d.ts","../src/plugin.ts"],"sourcesContent":["//#region src/types.d.ts\n/**\n * Minimal logger contract shared by every helper in the kit.\n *\n * Deliberately the *narrowest* shape in the fleet: some plugins declare\n * variadic loggers (`info(msg: string, ...args: unknown[])`), others\n * single-arg (`info(msg: string)`). Both are assignable to this. The kit\n * only ever calls with a single string.\n */\ninterface KitLogger {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n debug(msg: string): void;\n}\n//# sourceMappingURL=types.d.ts.map\n//#endregion\n//#region src/tools.d.ts\n/** Shape returned to OpenClaw from a tool `execute`. */\ninterface ToolResult {\n content: {\n type: \"text\";\n text: string;\n }[];\n details: unknown;\n isError?: boolean;\n}\ninterface ToolDef<TParameters = unknown> {\n name: string;\n description: string;\n label: string;\n parameters: TParameters;\n execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;\n}\n/** Deliberately model-safe validation/usage failure. Other exceptions stay private. */\ndeclare class PublicToolError extends Error {\n readonly name = \"PublicToolError\";\n}\ndeclare function publicToolError(message: string): PublicToolError;\n/** Wrap bounded JSON in the OpenClaw tool-result envelope. */\ndeclare function ok(data: unknown): ToolResult;\n/** Wrap an explicitly public error in a captured OpenClaw error envelope. */\ndeclare function errResult(message: string): ToolResult;\n/**\n * Define a tool whose unexpected exceptions never expose provider, filesystem,\n * credential, or transport diagnostics to the model. Throw `PublicToolError`\n * only for a message deliberately safe for model display.\n */\ndeclare function defineTool<TParameters>(def: {\n name: string;\n description: string;\n parameters: TParameters;\n handler: (params: Record<string, unknown>) => Promise<unknown>;\n}): ToolDef<TParameters>;\n//# sourceMappingURL=tools.d.ts.map\n//#endregion\n//#region src/daemon.d.ts\ninterface IpcResponse {\n ok: boolean;\n error?: {\n message?: string;\n };\n [key: string]: unknown;\n}\ninterface IpcClient {\n on(event: string, handler: (...args: unknown[]) => void): void;\n request(method: string, params: Record<string, unknown>): Promise<IpcResponse>;\n start(): void;\n stop(): void;\n}\ninterface IpcModule {\n IPCClient: new (socketPath: string, log: KitLogger) => IpcClient;\n}\ninterface ConnectToDaemonOptions {\n pluginId: string;\n capabilities?: readonly string[];\n onMessage?: (msg: Record<string, unknown>) => void;\n standaloneNote?: string;\n}\ninterface ConnectToDaemonDependencies {\n loadIpcModule?: () => Promise<IpcModule>;\n}\ndeclare function connectToDaemon(socketPath: string, log: KitLogger, options: ConnectToDaemonOptions, dependencies?: ConnectToDaemonDependencies): Promise<IpcClient | null>;\n//#endregion\n//#region src/sdk.d.ts\ninterface ResolveOpenClawSdkOptions {\n specifier?: string;\n exportName?: string;\n unresolvableNote?: string;\n}\ninterface ResolveOpenClawSdkDependencies {\n anchors?: readonly string[];\n globalPackageJsonPath?: string;\n requireFrom?: (anchor: string) => (specifier: string) => unknown;\n}\ndeclare function resolveOpenClawSdk<T = unknown>(log: KitLogger, options?: ResolveOpenClawSdkOptions, dependencies?: ResolveOpenClawSdkDependencies): T | null;\n//# sourceMappingURL=sdk.d.ts.map\n\n//#endregion\n//#region src/activation.d.ts\n/**\n * Canonical activation-flag key for a plugin: `__alfe<Name>PluginActivated`.\n *\n * Accepts a short name (`\"google\"`, `\"google-chat\"`) or a full package name\n * (`\"@alfe.ai/openclaw-teams\"`); scope + `openclaw-` prefix are stripped\n * and the remainder PascalCased:\n *\n * getActivationKey(\"google\") → \"__alfeGooglePluginActivated\"\n * getActivationKey(\"google-chat\") → \"__alfeGoogleChatPluginActivated\"\n * getActivationKey(\"@alfe.ai/openclaw-teams\") → \"__alfeTeamsPluginActivated\"\n */\ndeclare function getActivationKey(name: string): string;\n/** True when the activation flag for `key` is currently set. */\ndeclare function isActivated(key: string): boolean;\n/**\n * Clear the activation flag so a later start can run again.\n *\n * Call this LAST in stop/deactivate paths — after side effects are stopped\n * (see the ordering rule in the module doc). Also exported for soft-failure\n * paths inside a `guardedStart` fn that want to log at a custom level and\n * return normally instead of throwing.\n */\ndeclare function resetActivation(key: string): void;\n/**\n * Run a service start exactly once per activation cycle.\n *\n * - If the flag is already set, logs at debug and returns `false` (skipped).\n * - Otherwise sets the flag and runs `fn`.\n * - If `fn` throws synchronously OR returns a promise that rejects, the\n * flag is RESET (so a later activate can retry) and the error is logged.\n * Errors are not rethrown — matching fleet behavior where a failed start\n * must never crash the host's plugin loader.\n *\n * Returns `true` when the start was initiated (even if an async portion\n * later fails), `false` when skipped or when `fn` threw synchronously.\n *\n * Note: cleaning up partial side effects on failure is `fn`'s job (throw\n * only after tearing down what was started); the kit only guarantees the\n * flag reset happens after `fn` has failed — i.e. after `fn`'s own cleanup.\n */\ndeclare function guardedStart(key: string, log: KitLogger, fn: () => void | Promise<void>): boolean;\n//# sourceMappingURL=activation.d.ts.map\n\n//#endregion\nexport { type ConnectToDaemonDependencies, type ConnectToDaemonOptions, type IpcClient, type IpcResponse, type KitLogger, PublicToolError, type ResolveOpenClawSdkDependencies, type ResolveOpenClawSdkOptions, type ToolDef, type ToolResult, connectToDaemon, defineTool, errResult, getActivationKey, guardedStart, isActivated, ok, publicToolError, resetActivation, resolveOpenClawSdk };\n//# sourceMappingURL=index.d.ts.map"],"mappings":";;;;ACwCgB;;;;UDrBNC,UAAAA,CC+BW;SACkD,EAAA;QAG3B,EAAA,MAAA;IAAO,IAAA,EAAA,MAAA;EAAA,CAAA,EA4O7C;EA8CL,OAAA,EAAA,OAAA;SAxCe,CAAA,EAAA,OAAA;;UD7QNC,OC+SyB,CAAA,cAAA,OAAA,CAAA,CAAA;;;;cD3SrBC;wCAC0BC,4BAA4BC,QAAQJ;;;;;;UCQlE,MAAA,CAOiB;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MAEmB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OAAR,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACkD,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAJ7D,iBAAA,CAOyC;EA4O7C,MAAA,EAlPI,MAgST;EAAA,gBAAA,CAAA,EAAA,MAAA,GAAA,YAAA,GAAA,eAAA,GAAA,cAAA;cAxCe,CAAA,IAAA,EAtPK,OAsPL,CAtPa,OAsPb,CAAA,CAAA,EAAA,IAAA;uBAkCE,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,GAAA,IAAA,EAAA,OAAA,EAAA,EAAA,GAvRqD,OAuRrD,CAAA,OAAA,CAAA,CAAA,EAAA,IAAA;EAAiB,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,GAAA,IAAA,EAAA,OAAA,EAAA,EAAA,GAAA,IAAA,GApRS,OAoRT,CAAA,IAAA,CAAA,EAAA,QAAA,EAAA;;;;cAxC7B;;;;;gBAMU;kBAkCE"}
package/dist/plugin.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
- import { Type } from "@sinclair/typebox";
3
2
  import { resolveConfig } from "@alfe.ai/config";
4
- import { installToolErrorCapture } from "@alfe.ai/agent-api-client";
3
+ import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
4
+ import { defineTool, getActivationKey, guardedStart, publicToolError, resetActivation } from "@alfe.ai/openclaw-plugin-kit";
5
+ import { Type } from "@sinclair/typebox";
5
6
  //#region src/plugin.ts
6
7
  /**
7
8
  * @alfe/openclaw-mobile — OpenClaw native plugin
@@ -18,90 +19,79 @@ import { installToolErrorCapture } from "@alfe.ai/agent-api-client";
18
19
  * - mobile_call — initiate an outbound phone call
19
20
  */
20
21
  const pkg = createRequire(import.meta.url)("../package.json");
21
- function ok(data) {
22
- return {
23
- content: [{
24
- type: "text",
25
- text: JSON.stringify(data)
26
- }],
27
- details: data
28
- };
22
+ const SUPPORTED_COUNTRIES = [
23
+ "AU",
24
+ "US",
25
+ "CA",
26
+ "GB"
27
+ ];
28
+ const E164_PATTERN = "^\\+\\d{7,15}$";
29
+ const E164_RE = /^\+\d{7,15}$/u;
30
+ const NUMBER_SEARCH_RE = /^[A-Za-z0-9*+ -]+$/u;
31
+ const MAX_SEARCH_LENGTH = 32;
32
+ const MAX_SMS_LENGTH = 4400;
33
+ function requireString(params, field, options) {
34
+ const value = params[field];
35
+ if (typeof value !== "string" || value.length < 1 || value.length > options.maxLength) throw publicToolError(`${field} must be a non-empty string of at most ${String(options.maxLength)} characters`);
36
+ if (options.pattern && !options.pattern.test(value)) throw publicToolError(`${field} has an invalid format`);
37
+ return value;
29
38
  }
30
- function errResult(message) {
31
- return {
32
- content: [{
33
- type: "text",
34
- text: JSON.stringify({ error: message })
35
- }],
36
- details: { error: message }
37
- };
39
+ function optionalSearchQuery(params) {
40
+ if (params.query === void 0) return void 0;
41
+ return requireString(params, "query", {
42
+ maxLength: MAX_SEARCH_LENGTH,
43
+ pattern: NUMBER_SEARCH_RE
44
+ });
38
45
  }
39
- function defineTool(def) {
40
- return {
41
- name: def.name,
42
- description: def.description,
43
- label: def.name,
44
- parameters: def.parameters,
45
- execute: async (_toolCallId, params) => {
46
- try {
47
- return ok(await def.handler(params));
48
- } catch (e) {
49
- return errResult(e.message);
50
- }
51
- }
52
- };
46
+ function optionalCountry(params) {
47
+ if (params.country === void 0) return void 0;
48
+ return requireCountry(params, "country");
53
49
  }
54
- let apiUrl = "";
55
- let apiKey = "";
56
- async function mobileApi(method, path, body, query) {
57
- let url = `${apiUrl}${path}`;
58
- if (query) {
59
- const entries = Object.entries(query).filter((entry) => entry[1] !== void 0);
60
- if (entries.length > 0) url += `?${new URLSearchParams(entries).toString()}`;
61
- }
62
- const headers = {
63
- "Content-Type": "application/json",
64
- Authorization: `Bearer ${apiKey}`
65
- };
66
- const res = await fetch(url, {
67
- method,
68
- headers,
69
- body: body ? JSON.stringify(body) : void 0
50
+ function requireCountry(params, field) {
51
+ const value = params[field];
52
+ if (typeof value !== "string" || !SUPPORTED_COUNTRIES.includes(value)) throw publicToolError(`${field} must be one of ${SUPPORTED_COUNTRIES.join(", ")}`);
53
+ return value;
54
+ }
55
+ function requireE164(params, field) {
56
+ return requireString(params, field, {
57
+ maxLength: 16,
58
+ pattern: E164_RE
70
59
  });
71
- const json = await res.json();
72
- if (!res.ok) {
73
- const issues = json.issues;
74
- if (Array.isArray(issues) && issues.length > 0) {
75
- const details = issues.map((i) => {
76
- return `${i.path?.join(".") ?? "input"}: ${i.message ?? "invalid"}`;
77
- }).join("; ");
78
- throw new Error(`Validation error${details}`);
79
- }
80
- const errorMsg = typeof json.error === "string" ? json.error : typeof json.message === "string" ? json.message : `Mobile service returned ${String(res.status)}`;
81
- throw new Error(errorMsg);
82
- }
83
- if (json.data && typeof json.data === "object") return json.data;
84
- return json;
60
+ }
61
+ function requireConfirmation(params) {
62
+ if (params.confirm !== true) throw publicToolError("confirm must be true after the user explicitly approves the recurring charge");
63
+ }
64
+ const MOBILE_ACTIVATION_KEY = getActivationKey("mobile");
65
+ let client;
66
+ function getClient() {
67
+ if (!client) throw new Error("Mobile tools unavailable no API config resolved");
68
+ return client;
85
69
  }
86
70
  const mobileTools = [
87
71
  defineTool({
88
72
  name: "mobile_get_number",
89
73
  description: "Check if you have a phone number assigned. Returns the phone number, country code, monthly price, and status. If no number is assigned, use mobile_search_numbers and mobile_assign_number to get one.",
90
- parameters: Type.Object({}),
74
+ parameters: Type.Object({}, { additionalProperties: false }),
91
75
  handler: async () => {
92
- return mobileApi("GET", "/mobile/numbers");
76
+ return getClient().getMobileNumber();
93
77
  }
94
78
  }),
95
79
  defineTool({
96
80
  name: "mobile_search_numbers",
97
81
  description: "Search for available phone numbers that can be purchased. Returns a list of available numbers and the monthly price. Supported countries: AU (Australia), US (United States), CA (Canada), GB (United Kingdom).",
98
82
  parameters: Type.Object({
99
- country: Type.Optional(Type.String({ description: "Country code: AU, US, CA, or GB (default: AU)" })),
100
- query: Type.Optional(Type.String({ description: "Optional search pattern — area code or text to match (e.g., '415' or 'COOL')" }))
101
- }),
83
+ country: Type.Optional(Type.Union(SUPPORTED_COUNTRIES.map((country) => Type.Literal(country)), { description: "Country code: AU, US, CA, or GB (default: AU)" })),
84
+ query: Type.Optional(Type.String({
85
+ description: "Optional search pattern — area code or text to match (e.g., '415' or 'COOL')",
86
+ minLength: 1,
87
+ maxLength: MAX_SEARCH_LENGTH,
88
+ pattern: NUMBER_SEARCH_RE.source
89
+ }))
90
+ }, { additionalProperties: false }),
102
91
  handler: async (params) => {
103
- const { country, query } = params;
104
- return mobileApi("GET", "/mobile/numbers/search", void 0, {
92
+ const country = optionalCountry(params);
93
+ const query = optionalSearchQuery(params);
94
+ return getClient().searchMobileNumbers({
105
95
  country,
106
96
  query
107
97
  });
@@ -109,14 +99,22 @@ const mobileTools = [
109
99
  }),
110
100
  defineTool({
111
101
  name: "mobile_assign_number",
112
- description: "Purchase and assign a phone number to yourself. You must search for available numbers first using mobile_search_numbers, then pass the chosen phoneNumber and countryCode here. Your organisation must have a payment method on file. The number will be billed monthly to the organisation's account.",
102
+ description: "Purchase and assign a phone number to yourself. You must search for available numbers first using mobile_search_numbers, then pass the chosen phoneNumber and countryCode here. Your organisation must have a payment method on file. The number will be billed monthly to the organisation's account and may replace an active number. Set confirm=true only after the user explicitly approves the charge.",
113
103
  parameters: Type.Object({
114
- phoneNumber: Type.String({ description: "The phone number to purchase (from mobile_search_numbers results)" }),
115
- countryCode: Type.String({ description: "Country code: AU, US, CA, or GB" })
116
- }),
104
+ phoneNumber: Type.String({
105
+ description: "The phone number to purchase (from mobile_search_numbers results)",
106
+ minLength: 8,
107
+ maxLength: 16,
108
+ pattern: E164_PATTERN
109
+ }),
110
+ countryCode: Type.Union(SUPPORTED_COUNTRIES.map((country) => Type.Literal(country)), { description: "Country code: AU, US, CA, or GB" }),
111
+ confirm: Type.Literal(true, { description: "Must be true after the user explicitly approves the recurring charge" })
112
+ }, { additionalProperties: false }),
117
113
  handler: async (params) => {
118
- const { phoneNumber, countryCode } = params;
119
- return mobileApi("POST", "/mobile/numbers/assign", {
114
+ const phoneNumber = requireE164(params, "phoneNumber");
115
+ const countryCode = requireCountry(params, "countryCode");
116
+ requireConfirmation(params);
117
+ return getClient().assignMobileNumber({
120
118
  phoneNumber,
121
119
  countryCode
122
120
  });
@@ -124,22 +122,40 @@ const mobileTools = [
124
122
  }),
125
123
  defineTool({
126
124
  name: "mobile_release_number",
127
- description: "Release your currently assigned phone number. This will cancel the monthly subscription and the number will no longer be available for calls or SMS. This action cannot be undone — the same number may not be available again.",
128
- parameters: Type.Object({}),
129
- handler: async () => {
130
- return mobileApi("POST", "/mobile/numbers/release", {});
125
+ description: "Release your currently assigned phone number. This will cancel the monthly subscription and the number will no longer be available for calls or SMS. This action cannot be undone — the same number may not be available again. Copy the exact current number into confirmPhoneNumber only after the user approves.",
126
+ parameters: Type.Object({ confirmPhoneNumber: Type.String({
127
+ description: "Exact currently assigned number, copied to confirm permanent release",
128
+ minLength: 8,
129
+ maxLength: 16,
130
+ pattern: E164_PATTERN
131
+ }) }, { additionalProperties: false }),
132
+ handler: async (params) => {
133
+ const confirmPhoneNumber = requireE164(params, "confirmPhoneNumber");
134
+ const mobileClient = getClient();
135
+ if ((await mobileClient.getMobileNumber()).phoneNumber !== confirmPhoneNumber) throw publicToolError("confirmPhoneNumber does not match the currently assigned number");
136
+ return mobileClient.releaseMobileNumber();
131
137
  }
132
138
  }),
133
139
  defineTool({
134
140
  name: "mobile_send_sms",
135
141
  description: "Send an SMS text message from your phone number to the specified number. The recipient number must be in E.164 format (e.g., +12025551234).",
136
142
  parameters: Type.Object({
137
- to: Type.String({ description: "Recipient phone number in E.164 format (e.g., +12025551234)" }),
138
- body: Type.String({ description: "The text message content to send" })
139
- }),
143
+ to: Type.String({
144
+ description: "Recipient phone number in E.164 format (e.g., +12025551234)",
145
+ minLength: 8,
146
+ maxLength: 16,
147
+ pattern: E164_PATTERN
148
+ }),
149
+ body: Type.String({
150
+ description: "The text message content to send",
151
+ minLength: 1,
152
+ maxLength: MAX_SMS_LENGTH
153
+ })
154
+ }, { additionalProperties: false }),
140
155
  handler: async (params) => {
141
- const { to, body } = params;
142
- return mobileApi("POST", "/mobile/sms/send", {
156
+ const to = requireE164(params, "to");
157
+ const body = requireString(params, "body", { maxLength: MAX_SMS_LENGTH });
158
+ return getClient().sendSms({
143
159
  to,
144
160
  body
145
161
  });
@@ -148,10 +164,15 @@ const mobileTools = [
148
164
  defineTool({
149
165
  name: "mobile_call",
150
166
  description: "Make an outbound phone call to the specified number. When the recipient answers, they will be connected to your voice pipeline. The number must be in E.164 format (e.g., +12025551234).",
151
- parameters: Type.Object({ to: Type.String({ description: "Phone number to call in E.164 format (e.g., +12025551234)" }) }),
167
+ parameters: Type.Object({ to: Type.String({
168
+ description: "Phone number to call in E.164 format (e.g., +12025551234)",
169
+ minLength: 8,
170
+ maxLength: 16,
171
+ pattern: E164_PATTERN
172
+ }) }, { additionalProperties: false }),
152
173
  handler: async (params) => {
153
- const { to } = params;
154
- return mobileApi("POST", "/mobile/calls/outbound", { to });
174
+ const to = requireE164(params, "to");
175
+ return getClient().startOutboundCall({ to });
155
176
  }
156
177
  })
157
178
  ];
@@ -164,24 +185,27 @@ const plugin = {
164
185
  installToolErrorCapture(api, { plugin: "openclaw-mobile" });
165
186
  const log = api.logger;
166
187
  for (const tool of mobileTools) api.registerTool(tool);
167
- log.info(`Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((t) => t.name).join(", ")}`);
168
- if (!globalThis.__mobilePluginActivated) {
169
- globalThis.__mobilePluginActivated = true;
188
+ log.info(`Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((tool) => tool.name).join(", ")}`);
189
+ guardedStart(MOBILE_ACTIVATION_KEY, log, () => {
170
190
  log.info("Alfe Mobile plugin activating...");
171
191
  try {
172
192
  const config = resolveConfig();
173
- apiUrl = config.apiUrl;
174
- apiKey = config.apiKey;
193
+ client = new AgentApiClient({
194
+ apiUrl: config.apiUrl,
195
+ apiKey: config.apiKey
196
+ });
197
+ log.info("Mobile API client configured");
175
198
  } catch (err) {
176
199
  log.error(`Failed to resolve config: ${err instanceof Error ? err.message : String(err)}`);
177
200
  log.warn("Mobile tools will fail — no API config available");
201
+ resetActivation(MOBILE_ACTIVATION_KEY);
178
202
  }
179
- log.info(`Mobile API: ${apiUrl}`);
180
- }
203
+ });
181
204
  log.info("Alfe Mobile plugin activated");
182
205
  },
183
206
  deactivate(api) {
184
- globalThis.__mobilePluginActivated = false;
207
+ client = void 0;
208
+ resetActivation(MOBILE_ACTIVATION_KEY);
185
209
  api.logger.info("Alfe Mobile plugin deactivated");
186
210
  }
187
211
  };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * @alfe/openclaw-mobile — OpenClaw native plugin\n *\n * Registers mobile tools (SMS, calls, number management) with OpenClaw.\n * Tools call the mobile service API using the agent's API key for authentication.\n *\n * This plugin provides:\n * - mobile_get_number — check if a phone number is assigned\n * - mobile_search_numbers — search available numbers for purchase\n * - mobile_assign_number — purchase and assign a phone number\n * - mobile_release_number — release the assigned phone number\n * - mobile_send_sms — send an SMS message from the agent's phone number\n * - mobile_call — initiate an outbound phone call\n */\n\nimport { Type, type TSchema } from \"@sinclair/typebox\";\nimport { resolveConfig } from \"@alfe.ai/config\";\nimport { installToolErrorCapture } from \"@alfe.ai/agent-api-client\";\nimport { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\ninterface Logger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n debug(msg: string, ...args: unknown[]): void;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n registrationMode?: \"full\" | \"setup-only\" | \"setup-runtime\" | \"cli-metadata\";\n registerTool(tool: ToolDef): void;\n registerGatewayMethod(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;\n on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: { priority?: number }): void;\n}\n\n// ── Tool types ───────────────────────────────────────────────\n\ninterface ToolDef {\n name: string;\n description: string;\n label: string;\n parameters: TSchema;\n execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{\n content: { type: \"text\"; text: string }[];\n details: unknown;\n }>;\n}\n\nfunction ok(data: unknown) {\n return { content: [{ type: \"text\" as const, text: JSON.stringify(data) }], details: data };\n}\n\nfunction errResult(message: string) {\n return { content: [{ type: \"text\" as const, text: JSON.stringify({ error: message }) }], details: { error: message } };\n}\n\nfunction defineTool(def: {\n name: string;\n description: string;\n parameters: TSchema;\n handler: (params: Record<string, unknown>) => Promise<unknown>;\n}): ToolDef {\n return {\n name: def.name,\n description: def.description,\n label: def.name,\n parameters: def.parameters,\n execute: async (_toolCallId: string, params: Record<string, unknown>) => {\n try {\n const result = await def.handler(params);\n return ok(result);\n } catch (e) {\n return errResult((e as Error).message);\n }\n },\n };\n}\n\n// ── API client ───────────────────────────────────────────────\n\nlet apiUrl = \"\";\nlet apiKey = \"\";\n\nasync function mobileApi(\n method: string,\n path: string,\n body?: Record<string, unknown>,\n query?: Record<string, string | undefined>,\n): Promise<Record<string, unknown>> {\n let url = `${apiUrl}${path}`;\n if (query) {\n const entries = Object.entries(query).filter((entry): entry is [string, string] => entry[1] !== undefined);\n if (entries.length > 0) {\n url += `?${new URLSearchParams(entries).toString()}`;\n }\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n };\n\n const res = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const json = (await res.json()) as Record<string, unknown>;\n if (!res.ok) {\n // Extract Zod validation issues if present (returned as { message: \"CORE_VALIDATION_FAILED\", issues: [...] })\n const issues = json.issues as { path?: string[]; message?: string }[] | undefined;\n if (Array.isArray(issues) && issues.length > 0) {\n const details = issues\n .map((i) => {\n const field = i.path?.join(\".\") ?? \"input\";\n return `${field}: ${i.message ?? \"invalid\"}`;\n })\n .join(\"; \");\n throw new Error(`Validation error — ${details}`);\n }\n\n const errorMsg = typeof json.error === \"string\"\n ? json.error\n : typeof json.message === \"string\"\n ? json.message\n : `Mobile service returned ${String(res.status)}`;\n throw new Error(errorMsg);\n }\n\n // unwrap { data: ... } envelope if present\n if (json.data && typeof json.data === \"object\") {\n return json.data as Record<string, unknown>;\n }\n return json;\n}\n\n// ── Tool definitions ─────────────────────────────────────────\n\nconst mobileTools: ToolDef[] = [\n // ── Number management ───────────────────────────────────────\n defineTool({\n name: \"mobile_get_number\",\n description:\n \"Check if you have a phone number assigned. Returns the phone number, \" +\n \"country code, monthly price, and status. If no number is assigned, \" +\n \"use mobile_search_numbers and mobile_assign_number to get one.\",\n parameters: Type.Object({}),\n handler: async () => {\n return mobileApi(\"GET\", \"/mobile/numbers\");\n },\n }),\n\n defineTool({\n name: \"mobile_search_numbers\",\n description:\n \"Search for available phone numbers that can be purchased. \" +\n \"Returns a list of available numbers and the monthly price. \" +\n \"Supported countries: AU (Australia), US (United States), CA (Canada), GB (United Kingdom).\",\n parameters: Type.Object({\n country: Type.Optional(Type.String({ description: \"Country code: AU, US, CA, or GB (default: AU)\" })),\n query: Type.Optional(Type.String({ description: \"Optional search pattern — area code or text to match (e.g., '415' or 'COOL')\" })),\n }),\n handler: async (params) => {\n const { country, query } = params as { country?: string; query?: string };\n return mobileApi(\"GET\", \"/mobile/numbers/search\", undefined, { country, query });\n },\n }),\n\n defineTool({\n name: \"mobile_assign_number\",\n description:\n \"Purchase and assign a phone number to yourself. You must search for available \" +\n \"numbers first using mobile_search_numbers, then pass the chosen phoneNumber and \" +\n \"countryCode here. Your organisation must have a payment method on file. \" +\n \"The number will be billed monthly to the organisation's account.\",\n parameters: Type.Object({\n phoneNumber: Type.String({ description: \"The phone number to purchase (from mobile_search_numbers results)\" }),\n countryCode: Type.String({ description: \"Country code: AU, US, CA, or GB\" }),\n }),\n handler: async (params) => {\n const { phoneNumber, countryCode } = params as { phoneNumber: string; countryCode: string };\n return mobileApi(\"POST\", \"/mobile/numbers/assign\", { phoneNumber, countryCode });\n },\n }),\n\n defineTool({\n name: \"mobile_release_number\",\n description:\n \"Release your currently assigned phone number. This will cancel the monthly \" +\n \"subscription and the number will no longer be available for calls or SMS. \" +\n \"This action cannot be undone — the same number may not be available again.\",\n parameters: Type.Object({}),\n handler: async () => {\n return mobileApi(\"POST\", \"/mobile/numbers/release\", {});\n },\n }),\n\n // ── Messaging & calls ───────────────────────────────────────\n defineTool({\n name: \"mobile_send_sms\",\n description:\n \"Send an SMS text message from your phone number to the specified number. \" +\n \"The recipient number must be in E.164 format (e.g., +12025551234).\",\n parameters: Type.Object({\n to: Type.String({ description: \"Recipient phone number in E.164 format (e.g., +12025551234)\" }),\n body: Type.String({ description: \"The text message content to send\" }),\n }),\n handler: async (params) => {\n const { to, body } = params as { to: string; body: string };\n return mobileApi(\"POST\", \"/mobile/sms/send\", { to, body });\n },\n }),\n\n defineTool({\n name: \"mobile_call\",\n description:\n \"Make an outbound phone call to the specified number. \" +\n \"When the recipient answers, they will be connected to your voice pipeline. \" +\n \"The number must be in E.164 format (e.g., +12025551234).\",\n parameters: Type.Object({\n to: Type.String({ description: \"Phone number to call in E.164 format (e.g., +12025551234)\" }),\n }),\n handler: async (params) => {\n const { to } = params as { to: string };\n return mobileApi(\"POST\", \"/mobile/calls/outbound\", { to });\n },\n }),\n];\n\n// ── Plugin definition ────────────────────────────────────────\n\nconst plugin = {\n id: \"@alfe.ai/openclaw-mobile\",\n name: \"Alfe Mobile Plugin\",\n description: \"Mobile integration — phone number management, SMS, and phone calls via Twilio\",\n version: pkg.version,\n\n activate(api: OpenClawPluginApi) {\n // First thing, before any registerTool call: tool failures emit a\n // deterministic [ERROR] line the gateway's runtime-output monitor captures\n // to Sentry. See @alfe.ai/agent-api-client tool-error-capture.\n installToolErrorCapture(api, { plugin: \"openclaw-mobile\" });\n const log = api.logger;\n\n\n // Always register tools — OpenClaw may reload the plugin registry per-session\n for (const tool of mobileTools) {\n api.registerTool(tool);\n }\n log.info(`Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((t) => t.name).join(\", \")}`);\n\n // Only initialize config once (side effects)\n if (!(globalThis as Record<string, unknown>).__mobilePluginActivated) {\n (globalThis as Record<string, unknown>).__mobilePluginActivated = true;\n log.info(\"Alfe Mobile plugin activating...\");\n\n try {\n const config = resolveConfig();\n apiUrl = config.apiUrl;\n apiKey = config.apiKey;\n } catch (err) {\n log.error(`Failed to resolve config: ${err instanceof Error ? err.message : String(err)}`);\n log.warn(\"Mobile tools will fail — no API config available\");\n }\n\n log.info(`Mobile API: ${apiUrl}`);\n }\n\n log.info(\"Alfe Mobile plugin activated\");\n },\n\n deactivate(api: OpenClawPluginApi) {\n (globalThis as Record<string, unknown>).__mobilePluginActivated = false;\n api.logger.info(\"Alfe Mobile plugin deactivated\");\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * @alfe/openclaw-mobile — OpenClaw native plugin\n *\n * Registers mobile tools (SMS, calls, number management) with OpenClaw.\n * Tools call the mobile service API using the agent's API key for authentication.\n *\n * This plugin provides:\n * - mobile_get_number — check if a phone number is assigned\n * - mobile_search_numbers — search available numbers for purchase\n * - mobile_assign_number — purchase and assign a phone number\n * - mobile_release_number — release the assigned phone number\n * - mobile_send_sms — send an SMS message from the agent's phone number\n * - mobile_call — initiate an outbound phone call\n */\n\nimport { createRequire } from \"node:module\";\nimport { resolveConfig } from \"@alfe.ai/config\";\nimport { AgentApiClient, installToolErrorCapture } from \"@alfe.ai/agent-api-client\";\nimport {\n defineTool,\n getActivationKey,\n guardedStart,\n publicToolError,\n resetActivation,\n type ToolDef,\n} from \"@alfe.ai/openclaw-plugin-kit\";\nimport { Type, type TSchema } from \"@sinclair/typebox\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../package.json\") as { version: string };\n\nconst SUPPORTED_COUNTRIES = [\"AU\", \"US\", \"CA\", \"GB\"] as const;\nconst E164_PATTERN = \"^\\\\+\\\\d{7,15}$\";\nconst E164_RE = /^\\+\\d{7,15}$/u;\nconst NUMBER_SEARCH_RE = /^[A-Za-z0-9*+ -]+$/u;\nconst MAX_SEARCH_LENGTH = 32;\nconst MAX_SMS_LENGTH = 4_400;\n\ntype SupportedCountry = (typeof SUPPORTED_COUNTRIES)[number];\n\ninterface Logger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n debug(msg: string, ...args: unknown[]): void;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n registrationMode?: \"full\" | \"setup-only\" | \"setup-runtime\" | \"cli-metadata\";\n registerTool(tool: ToolDef<TSchema>): void;\n registerGatewayMethod(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;\n on(\n event: string,\n handler: (...args: unknown[]) => void | Promise<void>,\n options?: { priority?: number },\n ): void;\n}\n\nfunction requireString(\n params: Record<string, unknown>,\n field: string,\n options: { maxLength: number; pattern?: RegExp },\n): string {\n const value = params[field];\n if (typeof value !== \"string\" || value.length < 1 || value.length > options.maxLength) {\n throw publicToolError(\n `${field} must be a non-empty string of at most ${String(options.maxLength)} characters`,\n );\n }\n if (options.pattern && !options.pattern.test(value)) {\n throw publicToolError(`${field} has an invalid format`);\n }\n return value;\n}\n\nfunction optionalSearchQuery(params: Record<string, unknown>): string | undefined {\n if (params.query === undefined) return undefined;\n return requireString(params, \"query\", {\n maxLength: MAX_SEARCH_LENGTH,\n pattern: NUMBER_SEARCH_RE,\n });\n}\n\nfunction optionalCountry(params: Record<string, unknown>): SupportedCountry | undefined {\n if (params.country === undefined) return undefined;\n return requireCountry(params, \"country\");\n}\n\nfunction requireCountry(params: Record<string, unknown>, field: string): SupportedCountry {\n const value = params[field];\n if (typeof value !== \"string\" || !SUPPORTED_COUNTRIES.includes(value as SupportedCountry)) {\n throw publicToolError(`${field} must be one of ${SUPPORTED_COUNTRIES.join(\", \")}`);\n }\n return value as SupportedCountry;\n}\n\nfunction requireE164(params: Record<string, unknown>, field: string): string {\n return requireString(params, field, { maxLength: 16, pattern: E164_RE });\n}\n\nfunction requireConfirmation(params: Record<string, unknown>): void {\n if (params.confirm !== true) {\n throw publicToolError(\n \"confirm must be true after the user explicitly approves the recurring charge\",\n );\n }\n}\n\n// ── Activation guard ─────────────────────────────────────────\n\nconst MOBILE_ACTIVATION_KEY = getActivationKey(\"mobile\");\n\n// ── API client ───────────────────────────────────────────────\n\nlet client: AgentApiClient | undefined;\n\nfunction getClient(): AgentApiClient {\n if (!client) {\n throw new Error(\"Mobile tools unavailable — no API config resolved\");\n }\n return client;\n}\n\n// ── Tool definitions ─────────────────────────────────────────\n\nconst mobileTools: ToolDef<TSchema>[] = [\n // ── Number management ───────────────────────────────────────\n defineTool({\n name: \"mobile_get_number\",\n description:\n \"Check if you have a phone number assigned. Returns the phone number, \" +\n \"country code, monthly price, and status. If no number is assigned, \" +\n \"use mobile_search_numbers and mobile_assign_number to get one.\",\n parameters: Type.Object({}, { additionalProperties: false }),\n handler: async () => {\n return getClient().getMobileNumber();\n },\n }),\n\n defineTool({\n name: \"mobile_search_numbers\",\n description:\n \"Search for available phone numbers that can be purchased. \" +\n \"Returns a list of available numbers and the monthly price. \" +\n \"Supported countries: AU (Australia), US (United States), CA (Canada), GB (United Kingdom).\",\n parameters: Type.Object(\n {\n country: Type.Optional(\n Type.Union(\n SUPPORTED_COUNTRIES.map((country) => Type.Literal(country)),\n { description: \"Country code: AU, US, CA, or GB (default: AU)\" },\n ),\n ),\n query: Type.Optional(\n Type.String({\n description: \"Optional search pattern — area code or text to match (e.g., '415' or 'COOL')\",\n minLength: 1,\n maxLength: MAX_SEARCH_LENGTH,\n pattern: NUMBER_SEARCH_RE.source,\n }),\n ),\n },\n { additionalProperties: false },\n ),\n handler: async (params) => {\n const country = optionalCountry(params);\n const query = optionalSearchQuery(params);\n return getClient().searchMobileNumbers({ country, query });\n },\n }),\n\n defineTool({\n name: \"mobile_assign_number\",\n description:\n \"Purchase and assign a phone number to yourself. You must search for available \" +\n \"numbers first using mobile_search_numbers, then pass the chosen phoneNumber and \" +\n \"countryCode here. Your organisation must have a payment method on file. \" +\n \"The number will be billed monthly to the organisation's account and may replace \" +\n \"an active number. Set confirm=true only after the user explicitly approves the charge.\",\n parameters: Type.Object(\n {\n phoneNumber: Type.String({\n description: \"The phone number to purchase (from mobile_search_numbers results)\",\n minLength: 8,\n maxLength: 16,\n pattern: E164_PATTERN,\n }),\n countryCode: Type.Union(\n SUPPORTED_COUNTRIES.map((country) => Type.Literal(country)),\n { description: \"Country code: AU, US, CA, or GB\" },\n ),\n confirm: Type.Literal(true, {\n description: \"Must be true after the user explicitly approves the recurring charge\",\n }),\n },\n { additionalProperties: false },\n ),\n handler: async (params) => {\n const phoneNumber = requireE164(params, \"phoneNumber\");\n const countryCode = requireCountry(params, \"countryCode\");\n requireConfirmation(params);\n return getClient().assignMobileNumber({ phoneNumber, countryCode });\n },\n }),\n\n defineTool({\n name: \"mobile_release_number\",\n description:\n \"Release your currently assigned phone number. This will cancel the monthly \" +\n \"subscription and the number will no longer be available for calls or SMS. \" +\n \"This action cannot be undone — the same number may not be available again. \" +\n \"Copy the exact current number into confirmPhoneNumber only after the user approves.\",\n parameters: Type.Object(\n {\n confirmPhoneNumber: Type.String({\n description: \"Exact currently assigned number, copied to confirm permanent release\",\n minLength: 8,\n maxLength: 16,\n pattern: E164_PATTERN,\n }),\n },\n { additionalProperties: false },\n ),\n handler: async (params) => {\n const confirmPhoneNumber = requireE164(params, \"confirmPhoneNumber\");\n const mobileClient = getClient();\n const current = await mobileClient.getMobileNumber();\n if (current.phoneNumber !== confirmPhoneNumber) {\n throw publicToolError(\"confirmPhoneNumber does not match the currently assigned number\");\n }\n return mobileClient.releaseMobileNumber();\n },\n }),\n\n // ── Messaging & calls ───────────────────────────────────────\n defineTool({\n name: \"mobile_send_sms\",\n description:\n \"Send an SMS text message from your phone number to the specified number. \" +\n \"The recipient number must be in E.164 format (e.g., +12025551234).\",\n parameters: Type.Object(\n {\n to: Type.String({\n description: \"Recipient phone number in E.164 format (e.g., +12025551234)\",\n minLength: 8,\n maxLength: 16,\n pattern: E164_PATTERN,\n }),\n body: Type.String({\n description: \"The text message content to send\",\n minLength: 1,\n maxLength: MAX_SMS_LENGTH,\n }),\n },\n { additionalProperties: false },\n ),\n handler: async (params) => {\n const to = requireE164(params, \"to\");\n const body = requireString(params, \"body\", { maxLength: MAX_SMS_LENGTH });\n return getClient().sendSms({ to, body });\n },\n }),\n\n defineTool({\n name: \"mobile_call\",\n description:\n \"Make an outbound phone call to the specified number. \" +\n \"When the recipient answers, they will be connected to your voice pipeline. \" +\n \"The number must be in E.164 format (e.g., +12025551234).\",\n parameters: Type.Object(\n {\n to: Type.String({\n description: \"Phone number to call in E.164 format (e.g., +12025551234)\",\n minLength: 8,\n maxLength: 16,\n pattern: E164_PATTERN,\n }),\n },\n { additionalProperties: false },\n ),\n handler: async (params) => {\n const to = requireE164(params, \"to\");\n return getClient().startOutboundCall({ to });\n },\n }),\n];\n\n// ── Plugin definition ────────────────────────────────────────\n\nconst plugin = {\n id: \"@alfe.ai/openclaw-mobile\",\n name: \"Alfe Mobile Plugin\",\n description: \"Mobile integration — phone number management, SMS, and phone calls via Twilio\",\n version: pkg.version,\n\n activate(api: OpenClawPluginApi) {\n // First thing, before any registerTool call: tool failures emit a\n // deterministic [ERROR] line the gateway's runtime-output monitor captures\n // to Sentry. See @alfe.ai/agent-api-client tool-error-capture.\n installToolErrorCapture(api, { plugin: \"openclaw-mobile\" });\n const log = api.logger;\n\n // Always register tools — OpenClaw may reload the plugin registry per-session\n for (const tool of mobileTools) {\n api.registerTool(tool);\n }\n log.info(\n `Registered ${String(mobileTools.length)} mobile tools: ${mobileTools.map((tool) => tool.name).join(\", \")}`,\n );\n\n // Only initialize config once (side effects)\n guardedStart(MOBILE_ACTIVATION_KEY, log, () => {\n log.info(\"Alfe Mobile plugin activating...\");\n\n try {\n const config = resolveConfig();\n client = new AgentApiClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });\n log.info(\"Mobile API client configured\");\n } catch (err) {\n log.error(`Failed to resolve config: ${err instanceof Error ? err.message : String(err)}`);\n log.warn(\"Mobile tools will fail — no API config available\");\n // Reset so a later activate() can retry once config exists.\n resetActivation(MOBILE_ACTIVATION_KEY);\n }\n });\n\n log.info(\"Alfe Mobile plugin activated\");\n },\n\n deactivate(api: OpenClawPluginApi) {\n // Stop side effects BEFORE resetting the activation flag.\n client = undefined;\n resetActivation(MOBILE_ACTIVATION_KEY);\n api.logger.info(\"Alfe Mobile plugin deactivated\");\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.cjs";
2
+ export { plugin as default };
@@ -0,0 +1,2 @@
1
+ import { t as plugin } from "./plugin.js";
2
+ export { plugin as default };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "@alfe.ai/openclaw-mobile",
3
3
  "name": "Mobile",
4
- "description": "Outbound SMS and phone calls via Twilio",
4
+ "description": "Phone number management, outbound SMS, and calls via Twilio",
5
5
  "entry": "./dist/plugin.js",
6
6
  "activation": { "onStartup": false },
7
7
  "contracts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-mobile",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "OpenClaw mobile plugin for Alfe — phone number management, SMS, and calls via Twilio",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,9 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@sinclair/typebox": "^0.34.48",
31
- "@alfe.ai/agent-api-client": "0.13.0",
32
- "@alfe.ai/config": "0.3.0"
31
+ "@alfe.ai/agent-api-client": "0.15.0",
32
+ "@alfe.ai/config": "0.4.1",
33
+ "@alfe.ai/openclaw-plugin-kit": "0.2.0"
33
34
  },
34
35
  "homepage": "https://alfe.ai",
35
36
  "author": "Alfe (https://alfe.ai)",
@@ -45,6 +46,7 @@
45
46
  "build": "tsdown",
46
47
  "dev": "tsdown --watch",
47
48
  "test": "vitest run --passWithNoTests",
49
+ "typecheck": "tsc --noEmit",
48
50
  "lint": "eslint ."
49
51
  }
50
52
  }