@envarai/envarpay 0.2.0-alpha.11 → 0.2.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commerce/agent-setup.d.ts +18 -0
- package/dist/commerce/agent-setup.js +189 -0
- package/dist/commerce/cli.js +9 -2
- package/dist/commerce/setup.d.ts +4 -0
- package/dist/commerce/setup.js +27 -17
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Invitation {
|
|
2
|
+
version: 1;
|
|
3
|
+
agentId: string;
|
|
4
|
+
platformOrigin: string;
|
|
5
|
+
setupToken: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function invitation(value: any): Invitation;
|
|
8
|
+
export declare function agentSetup(options: {
|
|
9
|
+
file: string;
|
|
10
|
+
directory?: string;
|
|
11
|
+
python?: string;
|
|
12
|
+
home?: string;
|
|
13
|
+
container?: string;
|
|
14
|
+
origin?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
checkOnly?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/** Agent-driven installation. Secrets stay in local files; receipts come from Envar. */
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { createServer } from 'node:net';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { setupHermes, parseSetupBundle } from './setup.js';
|
|
8
|
+
import { CommerceError } from './types.js';
|
|
9
|
+
function privateJson(file) {
|
|
10
|
+
const s = lstatSync(file);
|
|
11
|
+
if (!s.isFile() || s.isSymbolicLink() || s.size > 16384 || (s.mode & 0o077))
|
|
12
|
+
throw new CommerceError('setup_private_file', 'Save the setup invitation in an owner-only file (chmod600).');
|
|
13
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
export function invitation(value) {
|
|
16
|
+
if (value?.version !== 1 || !/^[0-9a-f-]{36}$/.test(value.agentId) || !/^envar_setup_[A-Za-z0-9_-]{43}$/.test(value.setupToken))
|
|
17
|
+
throw new CommerceError('setup_invitation', 'Copy a fresh setup prompt.');
|
|
18
|
+
const url = new URL(value.platformOrigin);
|
|
19
|
+
if (url.protocol !== 'https:' || url.origin !== value.platformOrigin || url.username || url.password)
|
|
20
|
+
throw new CommerceError('setup_origin', 'Use the exact Envar HTTPS origin.');
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
async function request(invite, path, data) {
|
|
24
|
+
const r = await fetch(invite.platformOrigin + '/api/v1/agent-setup/' + path, { method: 'POST', redirect: 'error', headers: { Authorization: 'Bearer ' + invite.setupToken, 'Content-Type': 'application/json' }, body: JSON.stringify(data), signal: AbortSignal.timeout(45000) });
|
|
25
|
+
if (!r.ok)
|
|
26
|
+
throw new CommerceError('setup_' + path, `Envar ${path} returned ${r.status}. Check connection or copy a fresh prompt; do not report success.`);
|
|
27
|
+
const text = await r.text();
|
|
28
|
+
if (text.length > 65536)
|
|
29
|
+
throw new CommerceError('setup_response', 'Envar returned an oversized response.');
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
async function freePort() {
|
|
33
|
+
const socket = createServer();
|
|
34
|
+
await new Promise((resolve, reject) => { socket.once('error', reject); socket.listen(0, '127.0.0.1', resolve); });
|
|
35
|
+
const port = socket.address().port;
|
|
36
|
+
await new Promise(resolve => socket.close(() => resolve()));
|
|
37
|
+
return port;
|
|
38
|
+
}
|
|
39
|
+
function save(file, data) { writeFileSync(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); }
|
|
40
|
+
async function publicOrigin(root, port, agentId, supplied) {
|
|
41
|
+
if (supplied) {
|
|
42
|
+
const u = new URL(supplied);
|
|
43
|
+
if (u.protocol !== 'https:' || u.origin !== supplied)
|
|
44
|
+
throw Error('Use a public HTTPS origin');
|
|
45
|
+
return supplied;
|
|
46
|
+
}
|
|
47
|
+
// Use an available existing tunnel session, without deleting or rebinding any endpoint.
|
|
48
|
+
for (const apiPort of [4040, 4041, 4042, 4043]) {
|
|
49
|
+
try {
|
|
50
|
+
const api = `http://127.0.0.1:${apiPort}`;
|
|
51
|
+
const r = await fetch(api + '/api/tunnels', { signal: AbortSignal.timeout(1000) });
|
|
52
|
+
if (!r.ok)
|
|
53
|
+
continue;
|
|
54
|
+
const list = (await r.json());
|
|
55
|
+
const name = 'envarpay-' + agentId, addr = `http://127.0.0.1:${port}`;
|
|
56
|
+
const prior = list.tunnels?.find((t) => t.name === name);
|
|
57
|
+
if (prior) {
|
|
58
|
+
if (prior.config.addr !== addr)
|
|
59
|
+
continue;
|
|
60
|
+
return prior.public_url;
|
|
61
|
+
}
|
|
62
|
+
if (list.tunnels?.length >= 3)
|
|
63
|
+
continue;
|
|
64
|
+
const created = await fetch(api + '/api/tunnels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, proto: 'http', addr, schemes: ['https'], inspect: false }), signal: AbortSignal.timeout(12000) });
|
|
65
|
+
if (created.ok)
|
|
66
|
+
return (await created.json()).public_url;
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
70
|
+
// Outbound HTTPS works inside Docker too; never requires a Docker socket or host port mapping.
|
|
71
|
+
const log = join(root, 'tunnel.log');
|
|
72
|
+
if (existsSync(log))
|
|
73
|
+
privateLog(log);
|
|
74
|
+
const offset = existsSync(log) ? readFileSync(log, 'utf8').length : 0, fd = openSync(log, 'a', 0o600);
|
|
75
|
+
const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate'], { detached: true, stdio: ['ignore', fd, fd] });
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
let failed = false;
|
|
78
|
+
tunnel.on('error', () => { failed = true; });
|
|
79
|
+
tunnel.unref();
|
|
80
|
+
for (let i = 0; i < 45; i++) {
|
|
81
|
+
if (failed)
|
|
82
|
+
break;
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
84
|
+
const url = readFileSync(log, 'utf8').slice(offset).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
|
|
85
|
+
if (url) {
|
|
86
|
+
writeFileSync(join(root, 'tunnel-process.json'), JSON.stringify({ pid: tunnel.pid, origin: url, temporary: true }), { mode: 0o600 });
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (tunnel.pid)
|
|
91
|
+
try {
|
|
92
|
+
tunnel.kill();
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
throw new CommerceError('setup_https', 'Install cloudflared from its official distribution, or supply --origin with an existing HTTPS forwarder. Preserve other tunnels.');
|
|
96
|
+
}
|
|
97
|
+
function privateLog(file) { const s = lstatSync(file); if (!s.isFile() || s.isSymbolicLink() || (s.mode & 0o077))
|
|
98
|
+
throw new CommerceError('setup_private_file', 'Runtime log must be an owner-only regular file.'); }
|
|
99
|
+
export async function agentSetup(options) {
|
|
100
|
+
const invite = invitation(privateJson(resolve(options.file)));
|
|
101
|
+
const profile = resolve(options.directory ?? join(homedir(), '.envarpay', invite.agentId));
|
|
102
|
+
const root = profile + '.onboarding';
|
|
103
|
+
if (options.checkOnly) {
|
|
104
|
+
const result = await request(invite, 'status', {});
|
|
105
|
+
if (result.agent_id !== invite.agentId)
|
|
106
|
+
throw new CommerceError('setup_identity', 'Receipt belongs to another Agent.');
|
|
107
|
+
console.log(JSON.stringify(result, null, 2));
|
|
108
|
+
if (result.status !== 'connected')
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
113
|
+
if (lstatSync(root).isSymbolicLink() || (lstatSync(root).mode & 0o077))
|
|
114
|
+
throw new CommerceError('setup_directory', 'Use a private directory owned by this user.');
|
|
115
|
+
const manifest = join(root, 'setup.json');
|
|
116
|
+
if (existsSync(manifest) && privateJson(manifest).agentId !== invite.agentId)
|
|
117
|
+
throw new CommerceError('setup_identity', 'This directory belongs to another Agent.');
|
|
118
|
+
const bundle = parseSetupBundle(await request(invite, 'claim', {}));
|
|
119
|
+
if (bundle.agentId !== invite.agentId || bundle.platformOrigin !== invite.platformOrigin)
|
|
120
|
+
throw new CommerceError('setup_identity', 'Envar returned another Agent identity.');
|
|
121
|
+
const bundleFile = join(root, 'bundle.json');
|
|
122
|
+
if (existsSync(bundleFile))
|
|
123
|
+
privateJson(bundleFile);
|
|
124
|
+
writeFileSync(bundleFile, JSON.stringify(bundle), { mode: 0o600 });
|
|
125
|
+
if (!existsSync(profile)) {
|
|
126
|
+
let settings = existsSync(manifest) ? privateJson(manifest) : undefined;
|
|
127
|
+
if (!settings) {
|
|
128
|
+
const port = options.port ?? await freePort();
|
|
129
|
+
const origin = await publicOrigin(root, port, invite.agentId, options.origin);
|
|
130
|
+
settings = { agentId: invite.agentId, port, origin };
|
|
131
|
+
save(manifest, settings);
|
|
132
|
+
}
|
|
133
|
+
await setupHermes({ file: bundleFile, directory: profile, python: options.python, home: options.home, container: options.container, automatic: settings });
|
|
134
|
+
}
|
|
135
|
+
if (lstatSync(profile).isSymbolicLink() || (lstatSync(profile).mode & 0o077))
|
|
136
|
+
throw new CommerceError('setup_directory', 'The runtime must be a private directory.');
|
|
137
|
+
const connection = privateJson(join(profile, 'connection.json'));
|
|
138
|
+
privateLog(join(profile, 'envar.token'));
|
|
139
|
+
const launcher = lstatSync(join(profile, 'start.mjs'));
|
|
140
|
+
if (!launcher.isFile() || launcher.isSymbolicLink() || (launcher.mode & 0o077))
|
|
141
|
+
throw new CommerceError('setup_private_file', 'Use the original private runtime launcher.');
|
|
142
|
+
if (connection.agentId !== invite.agentId)
|
|
143
|
+
throw new CommerceError('setup_identity', 'Existing runtime belongs to another Agent.');
|
|
144
|
+
const policy = privateJson(join(profile, 'envar.json')).policy;
|
|
145
|
+
if (policy.agentId !== invite.agentId || policy.platformOrigin !== invite.platformOrigin)
|
|
146
|
+
throw new CommerceError('setup_identity', 'Existing integration belongs to another Agent.');
|
|
147
|
+
const running = join(root, 'process.json');
|
|
148
|
+
let started = false;
|
|
149
|
+
if (existsSync(running))
|
|
150
|
+
try {
|
|
151
|
+
process.kill(privateJson(running).pid, 0);
|
|
152
|
+
started = true;
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
if (started && readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
|
|
156
|
+
throw new CommerceError('setup_running', 'Existing runtime uses an earlier credential. Stop its recorded process, keep its ledger, then retry this invitation.');
|
|
157
|
+
if (!started) {
|
|
158
|
+
privateLog(join(profile, 'envar.token'));
|
|
159
|
+
writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
|
|
160
|
+
const log = join(root, 'runtime.log');
|
|
161
|
+
if (existsSync(log))
|
|
162
|
+
privateLog(log);
|
|
163
|
+
const fd = openSync(log, 'a', 0o600), child = spawn(process.execPath, [join(profile, 'start.mjs')], { detached: true, stdio: ['ignore', fd, fd] });
|
|
164
|
+
closeSync(fd);
|
|
165
|
+
child.unref();
|
|
166
|
+
writeFileSync(running, JSON.stringify({ pid: child.pid }), { mode: 0o600 });
|
|
167
|
+
}
|
|
168
|
+
let result;
|
|
169
|
+
for (let attempt = 0; attempt < 12; attempt++) {
|
|
170
|
+
try {
|
|
171
|
+
result = await request(invite, 'connect', { agentId: invite.agentId, url: connection.url, authToken: connection.authToken });
|
|
172
|
+
if (result.status === 'connected')
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (attempt === 11)
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
180
|
+
}
|
|
181
|
+
result = await request(invite, 'status', {});
|
|
182
|
+
if (result.status !== 'connected' || result.agent_id !== invite.agentId || !result.verified_at)
|
|
183
|
+
throw new CommerceError('setup_unverified', 'Platform did not verify this setup. Keep files and report the blocker.');
|
|
184
|
+
const output = { ...result, local_directory: profile, receipt_file: join(root, 'receipt.json'), scope: 'connected; not published; no payment', restart: `node ${JSON.stringify(join(profile, 'start.mjs'))}`, temporary_tunnel: !options.origin, automatic_restart: false };
|
|
185
|
+
if (existsSync(join(root, 'receipt.json')))
|
|
186
|
+
privateJson(join(root, 'receipt.json'));
|
|
187
|
+
writeFileSync(join(root, 'receipt.json'), JSON.stringify(output, null, 2) + '\n', { mode: 0o600 });
|
|
188
|
+
console.log(JSON.stringify(output, null, 2));
|
|
189
|
+
}
|
package/dist/commerce/cli.js
CHANGED
|
@@ -24,14 +24,15 @@ import { inspectLedger, inspectLocalAlerts } from './operations.js';
|
|
|
24
24
|
import { setupHermes } from './setup.js';
|
|
25
25
|
import { SkillDiscovery } from './discovery.js';
|
|
26
26
|
import { upgradeDiscovery, configurePayments } from './configure.js';
|
|
27
|
+
import { agentSetup } from './agent-setup.js';
|
|
27
28
|
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
28
29
|
config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
|
|
29
30
|
version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
|
|
30
31
|
port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
|
|
31
32
|
after: { type: 'string' }, limit: { type: 'string' }, 'refund-review': { type: 'boolean' }, alerts: { type: 'boolean' }, 'fail-on-alert': { type: 'boolean' }, 'integration-state': { type: 'string' }, 'no-integration': { type: 'boolean' },
|
|
32
|
-
file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' },
|
|
33
|
+
file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
|
|
33
34
|
} });
|
|
34
|
-
const usage = 'envarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
35
|
+
const usage = 'envarpay onboard --file invitation.json [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
35
36
|
const closers = [];
|
|
36
37
|
function privateFile(path) {
|
|
37
38
|
const stat = lstatSync(path);
|
|
@@ -91,6 +92,12 @@ async function run() {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
const command = positionals[0];
|
|
95
|
+
if (command === 'onboard') {
|
|
96
|
+
if (!values.file)
|
|
97
|
+
throw new CommerceError('setup_file', 'Save the private invitation and pass --file PATH.');
|
|
98
|
+
await agentSetup({ file: values.file, directory: values.directory, python: values.python, home: values['hermes-home'], container: values['docker-container'], origin: values.origin, port: values.port ? port(4020) : undefined, checkOnly: values.check });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
94
101
|
if (command === 'upgrade' && values.directory) {
|
|
95
102
|
upgradeDiscovery(values.directory, values['hermes-home']);
|
|
96
103
|
return;
|
package/dist/commerce/setup.d.ts
CHANGED
package/dist/commerce/setup.js
CHANGED
|
@@ -69,18 +69,16 @@ export async function setupHermes(options) {
|
|
|
69
69
|
const bundle = parseSetupBundle(JSON.parse(readFileSync(file, "utf8")));
|
|
70
70
|
if (options.container && !CONTAINER.test(options.container))
|
|
71
71
|
throw new CommerceError("setup_container", "Use a Docker container name");
|
|
72
|
-
if (!process.stdin.isTTY)
|
|
72
|
+
if (!process.stdin.isTTY && !options.automatic)
|
|
73
73
|
throw new CommerceError("setup_terminal", "Run setup in an interactive terminal to select skills");
|
|
74
74
|
chmodSync(file, 0o600);
|
|
75
|
-
const prompt = createInterface({
|
|
76
|
-
|
|
77
|
-
output: process.stdout,
|
|
78
|
-
});
|
|
75
|
+
const prompt = options.automatic ? undefined : createInterface({ input: process.stdin, output: process.stdout });
|
|
76
|
+
const output = options.automatic ? (..._args) => { } : console.log;
|
|
79
77
|
const ask = async (label, fallback = "") => (await prompt.question(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
|
|
80
78
|
let staging;
|
|
81
79
|
try {
|
|
82
|
-
|
|
83
|
-
if ((await ask("Continue? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
80
|
+
output(`\nConnect ${bundle.agentName} to ${bundle.platformOrigin}\nModel credentials and tasks remain on this machine. Start with free services; no wallet is created.\n`);
|
|
81
|
+
if (!options.automatic && (await ask("Continue? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
84
82
|
return;
|
|
85
83
|
const directory = resolve(options.directory ?? join(homedir(), ".envarpay", bundle.agentId));
|
|
86
84
|
if (existsSync(directory))
|
|
@@ -105,6 +103,18 @@ export async function setupHermes(options) {
|
|
|
105
103
|
if (!python)
|
|
106
104
|
throw new CommerceError("setup_runtime", "No compatible Hermes interpreter found in this container. Update Hermes or pass --python PATH.");
|
|
107
105
|
}
|
|
106
|
+
if (!python && options.automatic) {
|
|
107
|
+
for (const candidate of [process.env.VIRTUAL_ENV && join(process.env.VIRTUAL_ENV, "bin/python"), "/opt/hermes-latest/bin/python", "/opt/hermes/.venv/bin/python", join(homedir(), "hermes-agent/.venv/bin/python"), "python3"].filter(Boolean)) {
|
|
108
|
+
try {
|
|
109
|
+
execFileSync(candidate, ["-c", compatibility], { stdio: "pipe", timeout: 30000 });
|
|
110
|
+
python = candidate;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
if (!python)
|
|
116
|
+
throw new CommerceError("setup_runtime", "No compatible Hermes Python found. Pass its executable using --python.");
|
|
117
|
+
}
|
|
108
118
|
python ??= await ask("Hermes Python executable", join(homedir(), "hermes-agent", ".venv", "bin", "python"));
|
|
109
119
|
const probeSource = readFileSync(new URL("../../examples/hermes-setup-probe.py", import.meta.url), "utf8");
|
|
110
120
|
let probe;
|
|
@@ -134,25 +144,25 @@ export async function setupHermes(options) {
|
|
|
134
144
|
throw new CommerceError("setup_model", "Could not read Hermes. Check the container/Python path and configure an OpenAI-compatible model in Hermes first.");
|
|
135
145
|
}
|
|
136
146
|
const candidates = probe.skills.filter((s) => s.textPackage && ID.test(s.name));
|
|
137
|
-
|
|
138
|
-
candidates.forEach((skill, i) =>
|
|
139
|
-
|
|
140
|
-
const selected = (await ask("Optional: enable existing text Skills now (comma separated, Enter to skip)"))
|
|
147
|
+
output(`Model: ${probe.model}\nInstalled text packages (review their instructions before selecting):`);
|
|
148
|
+
candidates.forEach((skill, i) => output(`${i + 1}. ${skill.name} — ${skill.description.replace(/[\r\n\x1b]/g, " ").slice(0, 160)}`));
|
|
149
|
+
output('Newly installed Skills will be discovered privately. Enable each one from the Envar service page after reviewing text-only compatibility.');
|
|
150
|
+
const selected = (options.automatic ? "" : await ask("Optional: enable existing text Skills now (comma separated, Enter to skip)"))
|
|
141
151
|
.split(",")
|
|
142
152
|
.map((s) => s.trim()).filter(Boolean);
|
|
143
153
|
if (selected.length > 32 ||
|
|
144
154
|
new Set(selected).size !== selected.length ||
|
|
145
155
|
selected.some((name) => !candidates.some((s) => s.name === name)))
|
|
146
156
|
throw new CommerceError("setup_skills", "Choose installed skill names from the list");
|
|
147
|
-
|
|
157
|
+
output("This adapter supplies buyer text and bundled Markdown references. Browser, script, file and API tools are disabled. Original Hermes skills are preserved; a service copy is created.");
|
|
148
158
|
if (selected.length &&
|
|
149
159
|
(await ask("Do these selected skills work with text input and text output only? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
150
160
|
return;
|
|
151
|
-
const port = Number(await ask("Local service port", "4020"));
|
|
161
|
+
const port = options.automatic?.port ?? Number(await ask("Local service port", "4020"));
|
|
152
162
|
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
153
163
|
throw new CommerceError("setup_port", "Choose a local port from 1024 to 65535");
|
|
154
164
|
let origin;
|
|
155
|
-
const tunnelMode = await ask("Public HTTPS: use an existing ngrok session or your own reverse proxy? (ngrok/url)", "ngrok");
|
|
165
|
+
const tunnelMode = options.automatic ? "url" : await ask("Public HTTPS: use an existing ngrok session or your own reverse proxy? (ngrok/url)", "ngrok");
|
|
156
166
|
if (tunnelMode === "ngrok") {
|
|
157
167
|
const api = new URL(await ask("Local ngrok API", "http://127.0.0.1:4040"));
|
|
158
168
|
if (api.protocol !== "http:" ||
|
|
@@ -192,7 +202,7 @@ export async function setupHermes(options) {
|
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
else if (tunnelMode === "url") {
|
|
195
|
-
origin = await ask(`Public HTTPS origin (forward to http://127.0.0.1:${port})`);
|
|
205
|
+
origin = options.automatic?.origin ?? await ask(`Public HTTPS origin (forward to http://127.0.0.1:${port})`);
|
|
196
206
|
}
|
|
197
207
|
else
|
|
198
208
|
throw new CommerceError("setup_tunnel", "Choose ngrok or url");
|
|
@@ -334,10 +344,10 @@ export async function setupHermes(options) {
|
|
|
334
344
|
writeFileSync(join(staging, "README.md"), `# ${bundle.agentName}\n\nStart: node ${JSON.stringify(start)}\n\nKeep the terminal, Hermes, and HTTPS tunnel running. Import connection.json in the Envar setup page, then check the connection. Choose a skill and save a free service. Request publication; this process applies the selected service automatically. Return to Envar and check/publish.\n\nOnly free service updates are authorized. Receiving and payment setup is separate. Platform token expires ${bundle.expiresAt}. Keep the original files and ledger when restarting.\n`, { mode: 0o600, flag: "wx" });
|
|
335
345
|
renameSync(staging, directory);
|
|
336
346
|
staging = undefined;
|
|
337
|
-
|
|
347
|
+
output(`\nReady. Start the local service:\n${process.execPath} ${JSON.stringify(start)}\n\nThen import this connection file in Envar:\n${join(directory, "connection.json")}\n\nKeep the service and HTTPS tunnel online. No task or payment was performed.`);
|
|
338
348
|
}
|
|
339
349
|
finally {
|
|
340
|
-
prompt
|
|
350
|
+
prompt?.close();
|
|
341
351
|
if (staging)
|
|
342
352
|
rmSync(staging, { recursive: true, force: true });
|
|
343
353
|
}
|