@tokensmind/agent-network 0.1.0

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.
Files changed (63) hide show
  1. package/README.md +68 -0
  2. package/openclaw.plugin.json +32 -0
  3. package/package.json +55 -0
  4. package/skills/tokensmind-agent-network-runtime/SKILL.md +55 -0
  5. package/skills/tokensmind-agent-network-runtime/scripts/action_executor.py +112 -0
  6. package/skills/tokensmind-agent-network-runtime/scripts/action_support.py +106 -0
  7. package/skills/tokensmind-agent-network-runtime/scripts/action_validation.py +38 -0
  8. package/skills/tokensmind-agent-network-runtime/scripts/agent-network-runtime.mjs +54 -0
  9. package/skills/tokensmind-agent-network-runtime/scripts/agent_network_runtime.py +236 -0
  10. package/skills/tokensmind-agent-network-runtime/scripts/governance_actions.py +57 -0
  11. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-context.js +23 -0
  12. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-errors.js +55 -0
  13. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-executor.js +126 -0
  14. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-validation.js +61 -0
  15. package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +44 -0
  16. package/skills/tokensmind-agent-network-runtime/scripts/lib/api-client.js +76 -0
  17. package/skills/tokensmind-agent-network-runtime/scripts/lib/contact-action.js +154 -0
  18. package/skills/tokensmind-agent-network-runtime/scripts/lib/governance-actions.js +66 -0
  19. package/skills/tokensmind-agent-network-runtime/scripts/lib/messaging-actions.js +60 -0
  20. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/browser.js +26 -0
  21. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/connector.js +204 -0
  22. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/constants.js +10 -0
  23. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/credentialStore.js +162 -0
  24. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/crypto.js +25 -0
  25. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/deviceAuthorization.js +194 -0
  26. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/httpClient.js +54 -0
  27. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/portableStore.js +193 -0
  28. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/requestPolicy.js +55 -0
  29. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +176 -0
  30. package/skills/tokensmind-agent-network-runtime/scripts/lib/workflow-store.js +77 -0
  31. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/__init__.py +1 -0
  32. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/browser.py +27 -0
  33. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/connector.py +156 -0
  34. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/constants.py +12 -0
  35. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/credential_store.py +135 -0
  36. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/crypto.py +28 -0
  37. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/device_authorization.py +165 -0
  38. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/errors.py +9 -0
  39. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/http_client.py +50 -0
  40. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/portable_store.py +195 -0
  41. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/request_policy.py +85 -0
  42. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +143 -0
  43. package/src/action-context.js +23 -0
  44. package/src/action-errors.js +55 -0
  45. package/src/action-executor.js +126 -0
  46. package/src/action-validation.js +61 -0
  47. package/src/agent-actions.js +44 -0
  48. package/src/api-client.js +76 -0
  49. package/src/contact-action.js +154 -0
  50. package/src/governance-actions.js +66 -0
  51. package/src/index.js +70 -0
  52. package/src/messaging-actions.js +60 -0
  53. package/src/runtime/browser.js +26 -0
  54. package/src/runtime/connector.js +204 -0
  55. package/src/runtime/constants.js +10 -0
  56. package/src/runtime/credentialStore.js +162 -0
  57. package/src/runtime/crypto.js +25 -0
  58. package/src/runtime/deviceAuthorization.js +194 -0
  59. package/src/runtime/httpClient.js +54 -0
  60. package/src/runtime/portableStore.js +193 -0
  61. package/src/runtime/requestPolicy.js +55 -0
  62. package/src/runtime/systemCredentialStore.js +176 -0
  63. package/src/workflow-store.js +77 -0
@@ -0,0 +1,44 @@
1
+ import { actionFailure, requireInput } from './action-errors.js';
2
+ import { optionalLimit, text } from './action-validation.js';
3
+
4
+ export async function searchAgents(context) {
5
+ const query = text(context.input.query);
6
+ const limit = optionalLimit(context.input.limit);
7
+ const params = new URLSearchParams({ limit: String(limit) });
8
+ if (query) params.set('q', query);
9
+ return context.get(`/agent-network-api/agents?${params}`);
10
+ }
11
+
12
+ export async function getMyAgent(context) {
13
+ const agents = await context.get('/agent-network-api/agents?mine=1');
14
+ if (!Array.isArray(agents)) {
15
+ actionFailure('AGENT_LIST_PROTOCOL_ERROR', 'Agent Network returned an invalid Agent list.');
16
+ }
17
+ if (agents.length > 1) {
18
+ actionFailure('AGENT_ACCOUNT_INVARIANT', 'The account has more than one Agent profile.');
19
+ }
20
+ return agents[0] || null;
21
+ }
22
+
23
+ function profileInput(input) {
24
+ const profile = input.agent || input.profile || {};
25
+ const name = text(profile.name);
26
+ const description = text(profile.description);
27
+ if (!name || !description) requireInput(['agent.name', 'agent.description']);
28
+ return { name, description };
29
+ }
30
+
31
+ export async function ensureAgent(context) {
32
+ const existing = await getMyAgent(context);
33
+ if (existing) return { agent: existing, created: false };
34
+ const result = await context.mutate({
35
+ method: 'POST',
36
+ path: '/agent-network-api/agents',
37
+ body: profileInput(context.input),
38
+ label: 'agent:create',
39
+ });
40
+ if (!result?.agent) {
41
+ actionFailure('AGENT_CREATE_PROTOCOL_ERROR', 'Agent creation returned no Agent.');
42
+ }
43
+ return { agent: result.agent, created: true };
44
+ }
@@ -0,0 +1,76 @@
1
+ import os from 'node:os';
2
+ import { createBrowserOpener } from './runtime/browser.js';
3
+ import { createConnector } from './runtime/connector.js';
4
+ import { DEFAULT_BASE_URL } from './runtime/constants.js';
5
+ import { createCredentialStore } from './runtime/credentialStore.js';
6
+ import { createHttpClient } from './runtime/httpClient.js';
7
+ import { normalizeBaseUrl } from './runtime/requestPolicy.js';
8
+ import { createWorkflowStore } from './workflow-store.js';
9
+
10
+ const CLIENT_NAME = 'TokensMind Agent Network Runtime';
11
+ const CLIENT_VERSION = '0.1.0';
12
+
13
+ export class AuthorizationRequiredError extends Error {
14
+ constructor(verificationUrl) {
15
+ super('Complete Agent Network authorization in the browser, then retry the action.');
16
+ this.name = 'AuthorizationRequiredError';
17
+ this.status = 'authorization_required';
18
+ this.verificationUrl = verificationUrl;
19
+ }
20
+ }
21
+
22
+ function reportingBrowser({ browser, writeEvent }) {
23
+ return {
24
+ async open(url) {
25
+ try {
26
+ await browser.open(url);
27
+ writeEvent({ event: 'authorization_opened', verificationUrl: url });
28
+ } catch (error) {
29
+ writeEvent({ event: 'authorization_required', verificationUrl: url });
30
+ throw new AuthorizationRequiredError(url, { cause: error });
31
+ }
32
+ },
33
+ };
34
+ }
35
+
36
+ function resolveOptions(options) {
37
+ const platform = options.platform ?? process.platform;
38
+ return {
39
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
40
+ stateDir: options.stateDir,
41
+ platform,
42
+ env: options.env ?? process.env,
43
+ homeDir: options.homeDir ?? os.homedir(),
44
+ hostname: options.hostname ?? os.hostname(),
45
+ browser: options.browser ?? createBrowserOpener({ platform }),
46
+ http: options.http ?? createHttpClient(),
47
+ store: options.store ?? null,
48
+ writeEvent: options.writeEvent ?? (() => {}),
49
+ };
50
+ }
51
+
52
+ export function createActionApi(options = {}) {
53
+ const {
54
+ baseUrl, stateDir, platform, env, homeDir, hostname, browser, http, store, writeEvent,
55
+ } = resolveOptions(options);
56
+ const origin = normalizeBaseUrl(baseUrl);
57
+ const resolvedStore = store || createCredentialStore({ stateDir, origin, platform, env, homeDir });
58
+ const workflowStore = createWorkflowStore({ stateDir, origin, platform, homeDir });
59
+ const connector = createConnector({
60
+ baseUrl: origin,
61
+ browser: reportingBrowser({ browser, writeEvent }),
62
+ client: {
63
+ name: CLIENT_NAME,
64
+ version: CLIENT_VERSION,
65
+ deviceName: hostname,
66
+ platform,
67
+ },
68
+ http,
69
+ store: resolvedStore,
70
+ });
71
+ return {
72
+ request: (request) => connector.execute(request),
73
+ store: resolvedStore,
74
+ workflowStore,
75
+ };
76
+ }
@@ -0,0 +1,154 @@
1
+ import { actionFailure, requireInput, requireSelection } from './action-errors.js';
2
+ import { text } from './action-validation.js';
3
+ import { ensureAgent } from './agent-actions.js';
4
+
5
+ const STOPPED_REQUIREMENT_STATES = new Set(['paused', 'completed', 'cancelled']);
6
+
7
+ function exactName(agent, name) {
8
+ return text(agent?.name).toLocaleLowerCase() === name.toLocaleLowerCase();
9
+ }
10
+
11
+ function defaultTo(value, fallback) {
12
+ return value || fallback;
13
+ }
14
+
15
+ async function resolveTarget(context) {
16
+ const target = context.input.target || {};
17
+ const id = text(target.id);
18
+ if (id) return { id, name: text(target.name) };
19
+ const name = text(target.name);
20
+ if (!name) requireInput(['target.id or target.name']);
21
+ const agents = await context.get(
22
+ `/agent-network-api/agents?q=${encodeURIComponent(name)}&limit=20`,
23
+ );
24
+ if (!Array.isArray(agents) || agents.length === 0) {
25
+ actionFailure('TARGET_AGENT_NOT_FOUND', `No Agent matched "${name}".`);
26
+ }
27
+ const exact = agents.filter((agent) => exactName(agent, name));
28
+ const candidates = exact.length ? exact : agents;
29
+ if (candidates.length !== 1) {
30
+ requireSelection(candidates, `Choose the Agent to use for "${name}".`);
31
+ }
32
+ return candidates[0];
33
+ }
34
+
35
+ function newRequirementInput(input, agentId) {
36
+ const requirement = input.requirement || {};
37
+ const title = text(requirement.title);
38
+ const description = text(requirement.description);
39
+ if (!title || !description) requireInput(['requirement.title', 'requirement.description']);
40
+ return {
41
+ publisherAgentId: agentId,
42
+ title,
43
+ description,
44
+ requiredCapabilities: defaultTo(requirement.requiredCapabilities, []),
45
+ optionalCapabilities: defaultTo(requirement.optionalCapabilities, []),
46
+ industries: defaultTo(requirement.industries, []),
47
+ languages: defaultTo(requirement.languages, []),
48
+ budgetMin: requirement.budgetMin ?? null,
49
+ budgetMax: requirement.budgetMax ?? null,
50
+ currency: defaultTo(requirement.currency, 'USD'),
51
+ deadline: requirement.deadline ?? null,
52
+ visibility: requirement.visibility || 'public',
53
+ };
54
+ }
55
+
56
+ function validateContactInput(input) {
57
+ if (!text(input.message)) requireInput(['message']);
58
+ if (text(input.requirement?.id)) return;
59
+ newRequirementInput(input, text(input.target?.id) || 'pending-agent-id');
60
+ }
61
+
62
+ function contactResult({ agent, target, requirement, result }) {
63
+ const created = result?.created === true;
64
+ return {
65
+ agent,
66
+ target,
67
+ requirement,
68
+ conversation: result?.conversation,
69
+ message: result?.message || null,
70
+ created,
71
+ messageSent: created,
72
+ };
73
+ }
74
+
75
+ async function loadRequirement(context, agentId) {
76
+ const id = text(context.input.requirement?.id);
77
+ if (id) return context.get(`/agent-network-api/requirements/${encodeURIComponent(id)}`);
78
+ return context.mutate({
79
+ method: 'POST',
80
+ path: '/agent-network-api/requirements',
81
+ body: newRequirementInput(context.input, agentId),
82
+ label: 'requirement:create',
83
+ });
84
+ }
85
+
86
+ async function publishRequirement(context, requirement) {
87
+ if (requirement.status === 'open') return requirement;
88
+ if (STOPPED_REQUIREMENT_STATES.has(requirement.status)) {
89
+ actionFailure(
90
+ 'REQUIREMENT_NOT_OPENABLE',
91
+ `Requirement ${requirement.id} has terminal status ${requirement.status}.`,
92
+ { requirement },
93
+ );
94
+ }
95
+ if (requirement.status !== 'draft') {
96
+ actionFailure('REQUIREMENT_PROTOCOL_ERROR', 'Requirement has an unsupported status.');
97
+ }
98
+ const result = await context.mutate({
99
+ method: 'POST',
100
+ path: `/agent-network-api/requirements/${encodeURIComponent(requirement.id)}/publish`,
101
+ label: 'requirement:publish',
102
+ });
103
+ if (result?.requirement?.status !== 'open') {
104
+ actionFailure('REQUIREMENT_PUBLISH_PROTOCOL_ERROR', 'Requirement did not become open.');
105
+ }
106
+ return result.requirement;
107
+ }
108
+
109
+ async function eligibleRecommendation(context, requirement, target) {
110
+ const results = await context.get(
111
+ `/agent-network-api/requirements/${encodeURIComponent(requirement.id)}/recommendations`,
112
+ );
113
+ const recommendation = Array.isArray(results)
114
+ ? results.find((item) => item?.agent?.id === target.id)
115
+ : null;
116
+ if (!recommendation || recommendation.canReceiveNewConversations !== true) {
117
+ actionFailure(
118
+ 'TARGET_NOT_ELIGIBLE',
119
+ 'The requested Agent is not an eligible recommendation for this Requirement.',
120
+ { requirement, target },
121
+ );
122
+ }
123
+ return recommendation;
124
+ }
125
+
126
+ async function createConversation(context, { agent, requirement, target }) {
127
+ const message = text(context.input.message);
128
+ if (!message) requireInput(['message']);
129
+ return context.mutate({
130
+ method: 'POST',
131
+ path: '/agent-network-api/conversations',
132
+ body: {
133
+ requesterAgentId: agent.id,
134
+ requirementId: requirement.id,
135
+ targetAgentId: target.id,
136
+ initialMessage: {
137
+ clientMessageId: context.messageId('conversation:create'),
138
+ content: message,
139
+ },
140
+ },
141
+ label: 'conversation:create',
142
+ });
143
+ }
144
+
145
+ export async function contactAgent(context) {
146
+ validateContactInput(context.input);
147
+ const { agent } = await ensureAgent(context);
148
+ const target = await resolveTarget(context);
149
+ const draft = await loadRequirement(context, agent.id);
150
+ const requirement = await publishRequirement(context, draft);
151
+ await eligibleRecommendation(context, requirement, target);
152
+ const result = await createConversation(context, { agent, requirement, target });
153
+ return contactResult({ agent, target, requirement, result });
154
+ }
@@ -0,0 +1,66 @@
1
+ import { requireInput } from './action-errors.js';
2
+ import { requiredId, requiredText, text } from './action-validation.js';
3
+
4
+ export function blockAgent(context) {
5
+ const blockedAgentId = requiredId(context.input, 'blockedAgentId');
6
+ const reason = requiredText(context.input, 'reason');
7
+ return context.mutate({
8
+ method: 'POST',
9
+ path: '/agent-network-api/blocks',
10
+ body: {
11
+ blockedAgentId,
12
+ blockerAgentId: text(context.input.blockerAgentId) || null,
13
+ reason,
14
+ },
15
+ label: 'block:create',
16
+ });
17
+ }
18
+
19
+ export function unblockAgent(context) {
20
+ const blockedAgentId = requiredId(context.input, 'blockedAgentId');
21
+ const blockerAgentId = text(context.input.blockerAgentId);
22
+ const query = blockerAgentId
23
+ ? `?blockerAgentId=${encodeURIComponent(blockerAgentId)}`
24
+ : '';
25
+ return context.mutate({
26
+ method: 'DELETE',
27
+ path: `/agent-network-api/blocks/${encodeURIComponent(blockedAgentId)}${query}`,
28
+ label: 'block:remove',
29
+ });
30
+ }
31
+
32
+ export function report(context) {
33
+ const reasonCode = requiredText(context.input, 'reasonCode');
34
+ const targetFields = ['targetAgentId', 'conversationId', 'messageId'];
35
+ if (!targetFields.some((field) => context.input[field])) {
36
+ requireInput(targetFields, 'A report target is required.');
37
+ }
38
+ return context.mutate({
39
+ method: 'POST',
40
+ path: '/agent-network-api/reports',
41
+ body: {
42
+ reasonCode,
43
+ description: text(context.input.description),
44
+ reporterAgentId: text(context.input.reporterAgentId) || null,
45
+ targetAgentId: text(context.input.targetAgentId) || null,
46
+ conversationId: text(context.input.conversationId) || null,
47
+ messageId: context.input.messageId || null,
48
+ },
49
+ label: 'report:create',
50
+ });
51
+ }
52
+
53
+ export function appeal(context) {
54
+ const actionId = requiredId(context.input, 'actionId');
55
+ const statement = requiredText(context.input, 'statement');
56
+ return context.mutate({
57
+ method: 'POST',
58
+ path: '/agent-network-api/moderation-appeals',
59
+ body: {
60
+ actionId,
61
+ agentId: text(context.input.agentId) || null,
62
+ statement,
63
+ },
64
+ label: 'appeal:create',
65
+ });
66
+ }
package/src/index.js ADDED
@@ -0,0 +1,70 @@
1
+ import os from 'node:os';
2
+ import { defineToolPlugin } from 'openclaw/plugin-sdk/tool-plugin';
3
+ import { createActionApi } from './api-client.js';
4
+ import { createActionExecutor } from './action-executor.js';
5
+ import { DEFAULT_BASE_URL } from './runtime/constants.js';
6
+
7
+ const ACTION_SCHEMA = {
8
+ type: 'object',
9
+ additionalProperties: false,
10
+ required: ['operation'],
11
+ properties: {
12
+ operation: {
13
+ type: 'string',
14
+ enum: [
15
+ 'abandon_action', 'search_agents', 'get_my_agent', 'ensure_agent', 'contact_agent',
16
+ 'list_inbox', 'get_conversation', 'reply', 'mark_read',
17
+ 'withdraw_message', 'block_agent', 'unblock_agent', 'report', 'appeal',
18
+ ],
19
+ },
20
+ input: {
21
+ type: 'object',
22
+ description: 'Semantic business input for the selected operation.',
23
+ },
24
+ },
25
+ };
26
+
27
+ const CONFIG_SCHEMA = {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties: {
31
+ baseUrl: { type: 'string', format: 'uri', default: DEFAULT_BASE_URL },
32
+ stateDir: { type: 'string', description: 'Optional owner-private state directory.' },
33
+ },
34
+ };
35
+
36
+ const executors = new Map();
37
+
38
+ function executorKey(config) {
39
+ return JSON.stringify([config.baseUrl || DEFAULT_BASE_URL, config.stateDir || null]);
40
+ }
41
+
42
+ function getExecutor(config) {
43
+ const normalizedConfig = config || {};
44
+ const key = executorKey(normalizedConfig);
45
+ if (executors.has(key)) return executors.get(key);
46
+ const api = createActionApi({
47
+ baseUrl: normalizedConfig.baseUrl,
48
+ stateDir: normalizedConfig.stateDir,
49
+ hostname: os.hostname(),
50
+ writeEvent: () => {},
51
+ });
52
+ const workflowStore = api.workflowStore || null;
53
+ if (!workflowStore) throw new Error('Action executor requires a private workflow store');
54
+ const executor = createActionExecutor({ api, workflowStore });
55
+ executors.set(key, executor);
56
+ return executor;
57
+ }
58
+
59
+ export default defineToolPlugin({
60
+ id: 'tokensmind-agent-network-runtime',
61
+ name: 'TokensMind Agent Network Runtime',
62
+ description: 'Run high-level Agent Network operations with private authorization.',
63
+ configSchema: CONFIG_SCHEMA,
64
+ tools: (tool) => [tool({
65
+ name: 'agent_network_action',
66
+ description: 'Execute one high-level TokensMind Agent Network action.',
67
+ parameters: ACTION_SCHEMA,
68
+ execute: async (params, config) => getExecutor(config).execute(params),
69
+ })],
70
+ });
@@ -0,0 +1,60 @@
1
+ import { requireInput } from './action-errors.js';
2
+ import { optionalLimit, requiredId, requiredText, text } from './action-validation.js';
3
+
4
+ function queryString(input, fields) {
5
+ const params = new URLSearchParams();
6
+ for (const field of fields) {
7
+ if (input[field] !== undefined && input[field] !== null && input[field] !== '') {
8
+ params.set(field, String(input[field]));
9
+ }
10
+ }
11
+ params.set('limit', String(optionalLimit(input.limit)));
12
+ return params.toString();
13
+ }
14
+
15
+ export function listInbox(context) {
16
+ const query = queryString(context.input, ['cursor']);
17
+ return context.get(`/agent-network-api/inbox?${query}`);
18
+ }
19
+
20
+ export function getConversation(context) {
21
+ const id = requiredId(context.input, 'conversationId');
22
+ const query = queryString(context.input, ['cursor', 'before', 'direction']);
23
+ return context.get(
24
+ `/agent-network-api/conversations/${encodeURIComponent(id)}/messages?${query}`,
25
+ );
26
+ }
27
+
28
+ export function reply(context) {
29
+ const id = requiredId(context.input, 'conversationId');
30
+ const content = requiredText(context.input, 'content');
31
+ return context.mutate({
32
+ method: 'POST',
33
+ path: `/agent-network-api/conversations/${encodeURIComponent(id)}/messages`,
34
+ body: { clientMessageId: context.messageId('message:reply'), content },
35
+ label: 'message:reply',
36
+ });
37
+ }
38
+
39
+ export function markRead(context) {
40
+ const id = requiredId(context.input, 'conversationId');
41
+ const messageId = Number(context.input.lastReadMessageId);
42
+ if (!Number.isSafeInteger(messageId) || messageId <= 0) {
43
+ requireInput(['lastReadMessageId'], 'lastReadMessageId must be a positive integer.');
44
+ }
45
+ return context.mutate({
46
+ method: 'POST',
47
+ path: `/agent-network-api/conversations/${encodeURIComponent(id)}/read`,
48
+ body: { lastReadMessageId: messageId },
49
+ label: 'conversation:read',
50
+ });
51
+ }
52
+
53
+ export function withdrawMessage(context) {
54
+ const id = requiredId(context.input, 'messageId');
55
+ return context.mutate({
56
+ method: 'POST',
57
+ path: `/agent-network-api/messages/${encodeURIComponent(id)}/withdraw`,
58
+ label: 'message:withdraw',
59
+ });
60
+ }
@@ -0,0 +1,26 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ function browserCommand(platform) {
4
+ if (platform === 'darwin') return { command: 'open', args: [] };
5
+ if (platform === 'win32') return { command: 'cmd', args: ['/c', 'start', ''] };
6
+ return { command: 'xdg-open', args: [] };
7
+ }
8
+
9
+ export function createBrowserOpener({ platform = process.platform, spawnImpl = spawn } = {}) {
10
+ return {
11
+ open(url) {
12
+ return new Promise((resolve, reject) => {
13
+ const target = browserCommand(platform);
14
+ const child = spawnImpl(target.command, [...target.args, url], {
15
+ detached: true,
16
+ stdio: 'ignore',
17
+ });
18
+ child.on('error', reject);
19
+ child.on('spawn', () => {
20
+ child.unref();
21
+ resolve();
22
+ });
23
+ });
24
+ },
25
+ };
26
+ }