@elisym/cli 0.3.2 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env -S node --no-deprecation
2
- import { readFileSync, writeFileSync, readdirSync, statSync, rmSync, existsSync, mkdirSync, renameSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { dirname, join, basename } from 'node:path';
5
- import { SolanaPaymentStrategy, validateAgentName, ElisymIdentity, MediaService, RELAYS, formatSol, ElisymClient, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, serializeConfig, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet } from '@elisym/sdk';
6
- import { isEncrypted, parseConfig, encryptSecret } from '@elisym/sdk/node';
2
+ import { readFileSync, readdirSync, statSync, renameSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve, basename, relative, sep } from 'node:path';
4
+ import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet } from '@elisym/sdk';
5
+ import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYaml, writeSecrets, listAgents, loadAgent, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
7
6
  import { isAddress, createSolanaRpc, address } from '@solana/kit';
8
- import { nip19, getPublicKey, generateSecretKey } from 'nostr-tools';
7
+ import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
8
+ import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
+ import { isEncrypted } from '@elisym/sdk/node';
11
+ import { createHash } from 'node:crypto';
12
+ import { lookup } from 'node:dns/promises';
13
+ import { Socket } from 'node:net';
14
+ import pino from 'pino';
10
15
  import pLimit from 'p-limit';
11
- import YAML from 'yaml';
12
- import { spawn } from 'node:child_process';
13
- import { StringDecoder } from 'node:string_decoder';
16
+ import { parseSkillMd, validateSkillFrontmatter, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
14
17
  import { fileURLToPath } from 'node:url';
15
18
 
16
19
  var __defProp = Object.defineProperty;
@@ -22,69 +25,6 @@ var __export = (target, all) => {
22
25
  for (var name in all)
23
26
  __defProp(target, name, { get: all[name], enumerable: true });
24
27
  };
25
- function agentDir(name) {
26
- return join(AGENTS_ROOT, name);
27
- }
28
- function configPath(name) {
29
- return join(AGENTS_ROOT, name, "config.json");
30
- }
31
- function loadConfig(name, passphrase) {
32
- validateAgentName(name);
33
- const path = configPath(name);
34
- const raw = readFileSync(path, "utf-8");
35
- return parseConfig(raw, passphrase);
36
- }
37
- function saveConfig(config) {
38
- validateAgentName(config.identity.name);
39
- const dir = agentDir(config.identity.name);
40
- mkdirSync(dir, { recursive: true });
41
- writeFileSync(join(dir, "config.json"), serializeConfig(config), { mode: 384 });
42
- }
43
- function listAgents() {
44
- try {
45
- return readdirSync(AGENTS_ROOT).filter((name) => {
46
- try {
47
- statSync(join(AGENTS_ROOT, name, "config.json"));
48
- return true;
49
- } catch {
50
- return false;
51
- }
52
- });
53
- } catch {
54
- return [];
55
- }
56
- }
57
- function deleteAgent(name) {
58
- validateAgentName(name);
59
- const dir = agentDir(name);
60
- for (const file of ["config.json", "jobs.json"]) {
61
- try {
62
- const filePath = join(dir, file);
63
- const size = statSync(filePath).size;
64
- writeFileSync(filePath, Buffer.alloc(size, 0));
65
- } catch {
66
- }
67
- }
68
- try {
69
- for (const entry of readdirSync(dir)) {
70
- if (entry.startsWith("jobs.json.corrupt.")) {
71
- try {
72
- const fp = join(dir, entry);
73
- writeFileSync(fp, Buffer.alloc(statSync(fp).size, 0));
74
- } catch {
75
- }
76
- }
77
- }
78
- } catch {
79
- }
80
- rmSync(dir, { recursive: true, force: true });
81
- }
82
- var AGENTS_ROOT;
83
- var init_config = __esm({
84
- "src/config.ts"() {
85
- AGENTS_ROOT = join(homedir(), ".elisym", "agents");
86
- }
87
- });
88
28
 
89
29
  // src/commands/init.ts
90
30
  var init_exports = {};
@@ -92,20 +32,6 @@ __export(init_exports, {
92
32
  cmdInit: () => cmdInit,
93
33
  fetchModels: () => fetchModels
94
34
  });
95
- async function resolveImage(input, identity, media) {
96
- if (!input) {
97
- return "";
98
- }
99
- if (existsSync(input)) {
100
- console.log(` Uploading ${basename(input)}...`);
101
- const data = readFileSync(input);
102
- const blob = new Blob([data]);
103
- const url = await media.upload(identity, blob, basename(input));
104
- console.log(` Uploaded: ${url}`);
105
- return url;
106
- }
107
- return input;
108
- }
109
35
  async function fetchModels(provider, apiKey) {
110
36
  try {
111
37
  if (provider === "anthropic") {
@@ -138,31 +64,30 @@ async function fetchModels(provider, apiKey) {
138
64
  return FALLBACK_MODELS[provider] ?? ["gpt-4o"];
139
65
  }
140
66
  }
141
- async function cmdInit() {
67
+ function pickTarget(options) {
68
+ return options.local ? "project" : "home";
69
+ }
70
+ async function cmdInit(nameArg, options = {}) {
142
71
  const { default: inquirer } = await import('inquirer');
72
+ const cwd = process.cwd();
143
73
  console.log("\n elisym agent setup\n");
144
- const { name } = await inquirer.prompt([
145
- {
146
- type: "input",
147
- name: "name",
148
- message: "Agent name:",
149
- validate: (v) => {
150
- try {
151
- validateAgentName(v);
152
- return true;
153
- } catch (e) {
154
- return e.message;
155
- }
156
- }
157
- }
158
- ]);
159
- const existing = listAgents();
160
- if (existing.includes(name)) {
74
+ let template;
75
+ if (options.config) {
76
+ const configPath = resolve(cwd, options.config);
77
+ const raw = readFileSync(configPath, "utf-8");
78
+ template = ElisymYamlSchema.parse(YAML.parse(raw) ?? {});
79
+ console.log(` Loaded template from ${configPath}
80
+ `);
81
+ }
82
+ const agentName = await resolveAgentName(nameArg, inquirer);
83
+ const target = pickTarget(options);
84
+ const sameLocation = target === "home" ? resolveInHome(agentName) : resolveInProject(agentName, cwd);
85
+ if (sameLocation) {
161
86
  const { overwrite } = await inquirer.prompt([
162
87
  {
163
88
  type: "confirm",
164
89
  name: "overwrite",
165
- message: `Agent "${name}" already exists. Overwrite?`,
90
+ message: `Agent "${agentName}" already exists at ${sameLocation}. Overwrite secrets?`,
166
91
  default: false
167
92
  }
168
93
  ]);
@@ -170,7 +95,133 @@ async function cmdInit() {
170
95
  console.log("Aborted.");
171
96
  return;
172
97
  }
98
+ } else if (target === "project" && resolveInHome(agentName)) {
99
+ const { shadow } = await inquirer.prompt([
100
+ {
101
+ type: "confirm",
102
+ name: "shadow",
103
+ message: `A global agent "${agentName}" exists in ~/.elisym/${agentName}/. Create a project-local shadow?`,
104
+ default: true
105
+ }
106
+ ]);
107
+ if (!shadow) {
108
+ console.log("Aborted.");
109
+ return;
110
+ }
111
+ } else if (target === "home" && resolveInProject(agentName, cwd)) {
112
+ const { proceed } = await inquirer.prompt([
113
+ {
114
+ type: "confirm",
115
+ name: "proceed",
116
+ message: `A project-local agent "${agentName}" exists. Create a global agent with the same name?`,
117
+ default: true
118
+ }
119
+ ]);
120
+ if (!proceed) {
121
+ console.log("Aborted.");
122
+ return;
123
+ }
124
+ }
125
+ let yaml;
126
+ let promptedApiKey;
127
+ if (template) {
128
+ yaml = template;
129
+ } else {
130
+ const result = await promptYaml(inquirer);
131
+ yaml = result.yaml;
132
+ promptedApiKey = result.apiKey;
133
+ }
134
+ let llmApiKey;
135
+ if (yaml.llm) {
136
+ const envKey = yaml.llm.provider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
137
+ if (envKey) {
138
+ llmApiKey = envKey;
139
+ console.log(
140
+ ` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
141
+ );
142
+ } else if (promptedApiKey) {
143
+ llmApiKey = promptedApiKey;
144
+ } else {
145
+ const { apiKey } = await inquirer.prompt([
146
+ {
147
+ type: "password",
148
+ name: "apiKey",
149
+ message: `${yaml.llm.provider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
150
+ mask: "*"
151
+ }
152
+ ]);
153
+ llmApiKey = apiKey || void 0;
154
+ }
155
+ }
156
+ const { passphrase } = await inquirer.prompt([
157
+ {
158
+ type: "password",
159
+ name: "passphrase",
160
+ message: "Passphrase to encrypt secrets (leave empty to skip):",
161
+ mask: "*"
162
+ }
163
+ ]);
164
+ if (passphrase) {
165
+ const { confirmPassphrase } = await inquirer.prompt([
166
+ {
167
+ type: "password",
168
+ name: "confirmPassphrase",
169
+ message: "Confirm passphrase:",
170
+ mask: "*",
171
+ validate: (value) => value === passphrase || "Passphrases do not match"
172
+ }
173
+ ]);
174
+ }
175
+ const nostrSecretBytes = generateSecretKey();
176
+ const nostrPubkey = getPublicKey(nostrSecretBytes);
177
+ const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
178
+ const created = await createAgentDir({ target, name: agentName, cwd });
179
+ await writeYaml(created.dir, yaml);
180
+ await writeSecrets(
181
+ created.dir,
182
+ {
183
+ nostr_secret_key: nostrSecretHex,
184
+ llm_api_key: llmApiKey
185
+ },
186
+ passphrase || void 0
187
+ );
188
+ const npub = nip19.npubEncode(nostrPubkey);
189
+ console.log(`
190
+ Agent "${agentName}" created (${target}).`);
191
+ console.log(` Location: ${created.dir}`);
192
+ console.log(` Nostr: ${npub}`);
193
+ if (yaml.payments[0]?.address) {
194
+ console.log(` Solana: ${yaml.payments[0].address}`);
195
+ }
196
+ if (passphrase) {
197
+ console.log(" Secrets encrypted with your passphrase.");
173
198
  }
199
+ console.log();
200
+ }
201
+ async function resolveAgentName(nameArg, inquirer) {
202
+ if (nameArg) {
203
+ validateAgentName(nameArg);
204
+ return nameArg;
205
+ }
206
+ const { inputName } = await inquirer.prompt([
207
+ {
208
+ type: "input",
209
+ name: "inputName",
210
+ message: "Agent name:",
211
+ validate: (value) => {
212
+ try {
213
+ validateAgentName(value);
214
+ return true;
215
+ } catch (e) {
216
+ return e.message;
217
+ }
218
+ }
219
+ }
220
+ ]);
221
+ validateAgentName(inputName);
222
+ return inputName;
223
+ }
224
+ async function promptYaml(inquirer) {
174
225
  const { description } = await inquirer.prompt([
175
226
  {
176
227
  type: "input",
@@ -179,19 +230,27 @@ async function cmdInit() {
179
230
  default: "An elisym AI agent"
180
231
  }
181
232
  ]);
182
- const { pictureInput } = await inquirer.prompt([
233
+ const { displayName } = await inquirer.prompt([
234
+ {
235
+ type: "input",
236
+ name: "displayName",
237
+ message: "Display name (optional, for UI):",
238
+ default: ""
239
+ }
240
+ ]);
241
+ const { picture } = await inquirer.prompt([
183
242
  {
184
243
  type: "input",
185
- name: "pictureInput",
186
- message: "Avatar image (URL or local path, optional):",
244
+ name: "picture",
245
+ message: "Avatar file (relative to agent dir, e.g. ./avatar.png) or URL:",
187
246
  default: ""
188
247
  }
189
248
  ]);
190
- const { bannerInput } = await inquirer.prompt([
249
+ const { banner } = await inquirer.prompt([
191
250
  {
192
251
  type: "input",
193
- name: "bannerInput",
194
- message: "Banner image (URL or local path, optional):",
252
+ name: "banner",
253
+ message: "Banner file (relative to agent dir, e.g. ./header.png) or URL:",
195
254
  default: ""
196
255
  }
197
256
  ]);
@@ -201,23 +260,14 @@ async function cmdInit() {
201
260
  name: "solanaAddress",
202
261
  message: "Solana address for receiving payments (leave empty to skip):",
203
262
  default: "",
204
- validate: (v) => {
205
- if (!v) {
263
+ validate: (value) => {
264
+ if (!value) {
206
265
  return true;
207
266
  }
208
- return isAddress(v) || "Invalid Solana address";
267
+ return isAddress(value) || "Invalid Solana address";
209
268
  }
210
269
  }
211
270
  ]);
212
- const { network } = await inquirer.prompt([
213
- {
214
- type: "list",
215
- name: "network",
216
- message: "Solana network:",
217
- choices: [{ name: "devnet", value: "devnet" }],
218
- default: "devnet"
219
- }
220
- ]);
221
271
  const { llmProvider } = await inquirer.prompt([
222
272
  {
223
273
  type: "list",
@@ -229,16 +279,21 @@ async function cmdInit() {
229
279
  ]
230
280
  }
231
281
  ]);
232
- const { apiKey } = await inquirer.prompt([
233
- {
234
- type: "password",
235
- name: "apiKey",
236
- message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
237
- mask: "*"
238
- }
239
- ]);
282
+ const envKey = llmProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
283
+ let apiKey = envKey;
284
+ if (!apiKey) {
285
+ const { promptedKey } = await inquirer.prompt([
286
+ {
287
+ type: "password",
288
+ name: "promptedKey",
289
+ message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
290
+ mask: "*"
291
+ }
292
+ ]);
293
+ apiKey = promptedKey || void 0;
294
+ }
240
295
  console.log(" Fetching available models...");
241
- const models = await fetchModels(llmProvider, apiKey);
296
+ const models = await fetchModels(llmProvider, apiKey ?? "");
242
297
  const { model } = await inquirer.prompt([
243
298
  {
244
299
  type: "list",
@@ -255,77 +310,21 @@ async function cmdInit() {
255
310
  default: 4096
256
311
  }
257
312
  ]);
258
- const { passphrase } = await inquirer.prompt([
259
- {
260
- type: "password",
261
- name: "passphrase",
262
- message: "Passphrase to encrypt secrets (leave empty to skip):",
263
- mask: "*"
264
- }
265
- ]);
266
- if (passphrase) {
267
- const { confirmPassphrase } = await inquirer.prompt([
268
- {
269
- type: "password",
270
- name: "confirmPassphrase",
271
- message: "Confirm passphrase:",
272
- mask: "*",
273
- validate: (v) => v === passphrase ? true : "Passphrases do not match"
274
- }
275
- ]);
276
- }
277
- const nostrSecretKey = generateSecretKey();
278
- const nostrPubkey = getPublicKey(nostrSecretKey);
279
- const nostrSecretHex = Buffer.from(nostrSecretKey).toString("hex");
280
- const identity = ElisymIdentity.fromHex(nostrSecretHex);
281
- const media = new MediaService();
282
- let picture = "";
283
- let banner = "";
284
- if (pictureInput || bannerInput) {
285
- console.log();
286
- if (pictureInput) {
287
- picture = await resolveImage(pictureInput, identity, media);
288
- }
289
- if (bannerInput) {
290
- banner = await resolveImage(bannerInput, identity, media);
291
- }
292
- }
293
- const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
294
- const config = {
295
- identity: {
296
- secret_key: protect(nostrSecretHex),
297
- name,
298
- description,
299
- picture: picture || void 0,
300
- banner: banner || void 0
301
- },
313
+ const yaml = ElisymYamlSchema.parse({
314
+ display_name: displayName || void 0,
315
+ description,
316
+ picture: picture || void 0,
317
+ banner: banner || void 0,
302
318
  relays: [...RELAYS],
303
- payments: solanaAddress ? [{ chain: "solana", network, address: solanaAddress }] : void 0,
304
- llm: {
305
- provider: llmProvider,
306
- api_key: protect(apiKey),
307
- model,
308
- max_tokens: maxTokens
309
- }
310
- };
311
- saveConfig(config);
312
- const npub = nip19.npubEncode(nostrPubkey);
313
- console.log(`
314
- Agent "${name}" created.`);
315
- console.log(` Nostr: ${npub}`);
316
- if (solanaAddress) {
317
- console.log(` Solana: ${solanaAddress}`);
318
- }
319
- if (passphrase) {
320
- console.log(" Secrets encrypted with your passphrase.");
321
- }
322
- console.log(` Config: ~/.elisym/agents/${name}/config.json
323
- `);
319
+ payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
320
+ llm: { provider: llmProvider, model, max_tokens: maxTokens },
321
+ security: {}
322
+ });
323
+ return { yaml, apiKey: envKey ? void 0 : apiKey };
324
324
  }
325
325
  var FALLBACK_MODELS;
326
326
  var init_init = __esm({
327
327
  "src/commands/init.ts"() {
328
- init_config();
329
328
  FALLBACK_MODELS = {
330
329
  anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
331
330
  openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
@@ -335,41 +334,32 @@ var init_init = __esm({
335
334
 
336
335
  // src/index.ts
337
336
  init_init();
338
-
339
- // src/commands/profile.ts
340
- init_config();
341
- async function resolveImage2(input, identity, media) {
342
- if (!input) {
343
- return "";
344
- }
345
- if (existsSync(input)) {
346
- console.log(` Uploading ${basename(input)}...`);
347
- const data = readFileSync(input);
348
- const blob = new Blob([data]);
349
- const url = await media.upload(identity, blob, basename(input));
350
- console.log(` Uploaded: ${url}`);
351
- return url;
352
- }
353
- return input;
354
- }
355
337
  async function cmdProfile(name) {
356
338
  const { default: inquirer } = await import('inquirer');
339
+ const cwd = process.cwd();
357
340
  if (!name) {
358
- const agents = listAgents();
341
+ const agents = await listAgents(cwd);
359
342
  if (agents.length === 0) {
360
343
  console.error("No agents found. Run `elisym init` first.");
361
344
  process.exit(1);
362
345
  }
363
346
  const { selected } = await inquirer.prompt([
364
- { type: "list", name: "selected", message: "Select agent:", choices: agents }
347
+ {
348
+ type: "list",
349
+ name: "selected",
350
+ message: "Select agent:",
351
+ choices: agents.map((agent) => ({
352
+ name: `${agent.name} (${agent.source})`,
353
+ value: agent.name
354
+ }))
355
+ }
365
356
  ]);
366
357
  name = selected;
367
358
  }
368
359
  const passphrase = process.env.ELISYM_PASSPHRASE;
369
- const config = loadConfig(name, passphrase);
370
- const identity = ElisymIdentity.fromHex(config.identity.secret_key);
360
+ const loaded = await loadAgent(name, cwd, passphrase);
371
361
  console.log(`
372
- Editing agent "${name}"
362
+ Editing agent "${name}" (${loaded.source})
373
363
  `);
374
364
  let done = false;
375
365
  while (!done) {
@@ -379,13 +369,16 @@ async function cmdProfile(name) {
379
369
  name: "section",
380
370
  message: "What to edit?",
381
371
  choices: [
382
- { name: `Profile (name: ${config.identity.name})`, value: "profile" },
383
372
  {
384
- name: `Wallet (${config.payments?.[0]?.address ?? "not configured"})`,
373
+ name: `Profile (description: ${truncate(loaded.yaml.description ?? "")})`,
374
+ value: "profile"
375
+ },
376
+ {
377
+ name: `Wallet (${loaded.yaml.payments[0]?.address ?? "not configured"})`,
385
378
  value: "wallet"
386
379
  },
387
380
  {
388
- name: `LLM (${config.llm?.provider ?? "not configured"} / ${config.llm?.model ?? "-"})`,
381
+ name: `LLM (${loaded.yaml.llm?.provider ?? "not configured"} / ${loaded.yaml.llm?.model ?? "-"})`,
389
382
  value: "llm"
390
383
  },
391
384
  { name: "Done", value: "done" }
@@ -398,70 +391,64 @@ async function cmdProfile(name) {
398
391
  }
399
392
  if (section === "profile") {
400
393
  const answers = await inquirer.prompt([
394
+ {
395
+ type: "input",
396
+ name: "displayName",
397
+ message: "Display name (for UI):",
398
+ default: loaded.yaml.display_name ?? ""
399
+ },
401
400
  {
402
401
  type: "input",
403
402
  name: "description",
404
403
  message: "Description:",
405
- default: config.identity.description ?? ""
404
+ default: loaded.yaml.description ?? ""
406
405
  },
407
406
  {
408
407
  type: "input",
409
408
  name: "picture",
410
- message: "Avatar (URL or local path):",
411
- default: config.identity.picture ?? ""
409
+ message: "Avatar (relative path or URL, empty to clear):",
410
+ default: loaded.yaml.picture ?? ""
412
411
  },
413
412
  {
414
413
  type: "input",
415
414
  name: "banner",
416
- message: "Banner (URL or local path):",
417
- default: config.identity.banner ?? ""
415
+ message: "Banner (relative path or URL, empty to clear):",
416
+ default: loaded.yaml.banner ?? ""
418
417
  }
419
418
  ]);
420
- config.identity.description = answers.description || void 0;
421
- const media = new MediaService();
422
- if (answers.picture) {
423
- config.identity.picture = await resolveImage2(answers.picture, identity, media);
424
- } else {
425
- config.identity.picture = void 0;
426
- }
427
- if (answers.banner) {
428
- config.identity.banner = await resolveImage2(answers.banner, identity, media);
429
- } else {
430
- config.identity.banner = void 0;
431
- }
432
- saveConfig(config);
419
+ const nextYaml = {
420
+ ...loaded.yaml,
421
+ display_name: answers.displayName || void 0,
422
+ description: answers.description ?? "",
423
+ picture: answers.picture || void 0,
424
+ banner: answers.banner || void 0
425
+ };
426
+ await writeYaml(loaded.dir, nextYaml);
427
+ loaded.yaml = nextYaml;
433
428
  console.log(" Profile updated.\n");
434
429
  }
435
430
  if (section === "wallet") {
436
- const currentAddress = config.payments?.[0]?.address ?? "";
437
- const currentNetwork = config.payments?.[0]?.network ?? "devnet";
431
+ const current = loaded.yaml.payments[0];
438
432
  const answers = await inquirer.prompt([
439
433
  {
440
434
  type: "input",
441
435
  name: "address",
442
- message: "Solana address:",
443
- default: currentAddress,
444
- validate: (v) => {
445
- if (!v) {
436
+ message: "Solana address (empty to clear):",
437
+ default: current?.address ?? "",
438
+ validate: (value) => {
439
+ if (!value) {
446
440
  return true;
447
441
  }
448
- return isAddress(v) || "Invalid Solana address";
442
+ return isAddress(value) || "Invalid Solana address";
449
443
  }
450
- },
451
- {
452
- type: "list",
453
- name: "network",
454
- message: "Network:",
455
- choices: ["devnet"],
456
- default: currentNetwork
457
444
  }
458
445
  ]);
459
- if (answers.address) {
460
- config.payments = [{ chain: "solana", network: answers.network, address: answers.address }];
461
- } else {
462
- config.payments = void 0;
463
- }
464
- saveConfig(config);
446
+ const nextYaml = {
447
+ ...loaded.yaml,
448
+ payments: answers.address ? [{ chain: "solana", network: "devnet", address: answers.address }] : []
449
+ };
450
+ await writeYaml(loaded.dir, nextYaml);
451
+ loaded.yaml = nextYaml;
465
452
  console.log(" Wallet updated.\n");
466
453
  }
467
454
  if (section === "llm") {
@@ -474,33 +461,29 @@ async function cmdProfile(name) {
474
461
  { name: "Anthropic (Claude)", value: "anthropic" },
475
462
  { name: "OpenAI (GPT)", value: "openai" }
476
463
  ],
477
- default: config.llm?.provider ?? "anthropic"
464
+ default: loaded.yaml.llm?.provider ?? "anthropic"
478
465
  }
479
466
  ]);
480
467
  const { apiKey } = await inquirer.prompt([
481
468
  {
482
469
  type: "password",
483
470
  name: "apiKey",
484
- message: `API key (leave empty to keep current):`,
471
+ message: "API key (leave empty to keep current):",
485
472
  mask: "*"
486
473
  }
487
474
  ]);
488
- const effectiveKey = apiKey || config.llm?.api_key || "";
489
- let plainKey = effectiveKey;
490
- if (isEncrypted(plainKey) && passphrase) {
491
- const { decryptSecret } = await import('@elisym/sdk/node');
492
- plainKey = decryptSecret(plainKey, passphrase);
493
- }
475
+ const currentKeyPlain = loaded.secrets.llm_api_key && !isEncrypted(loaded.secrets.llm_api_key) ? loaded.secrets.llm_api_key : "";
476
+ const probeKey = apiKey || currentKeyPlain;
494
477
  console.log(" Fetching available models...");
495
478
  const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
496
- const models = await fetchModels2(llmProvider, plainKey);
479
+ const models = await fetchModels2(llmProvider, probeKey);
497
480
  const { model } = await inquirer.prompt([
498
481
  {
499
482
  type: "list",
500
483
  name: "model",
501
484
  message: "Model:",
502
485
  choices: models,
503
- default: config.llm?.model
486
+ default: loaded.yaml.llm?.model
504
487
  }
505
488
  ]);
506
489
  const { maxTokens } = await inquirer.prompt([
@@ -508,26 +491,109 @@ async function cmdProfile(name) {
508
491
  type: "number",
509
492
  name: "maxTokens",
510
493
  message: "Max tokens:",
511
- default: config.llm?.max_tokens ?? 4096
494
+ default: loaded.yaml.llm?.max_tokens ?? 4096
512
495
  }
513
496
  ]);
514
- const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
515
- config.llm = {
516
- provider: llmProvider,
517
- api_key: apiKey ? protect(apiKey) : effectiveKey,
518
- model,
519
- max_tokens: maxTokens
520
- };
521
- saveConfig(config);
497
+ const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
498
+ const nextYaml = { ...loaded.yaml, llm: nextLlm };
499
+ await writeYaml(loaded.dir, nextYaml);
500
+ loaded.yaml = nextYaml;
501
+ if (apiKey) {
502
+ await writeSecrets(loaded.dir, { ...loaded.secrets, llm_api_key: apiKey }, passphrase);
503
+ loaded.secrets = { ...loaded.secrets, llm_api_key: apiKey };
504
+ }
522
505
  console.log(" LLM updated.\n");
523
506
  }
524
507
  }
525
508
  console.log(` Agent "${name}" saved.
526
509
  `);
527
510
  }
528
-
529
- // src/commands/start.ts
530
- init_config();
511
+ function truncate(value, max = 40) {
512
+ if (value.length <= max) {
513
+ return value;
514
+ }
515
+ return value.slice(0, max - 1) + "\u2026";
516
+ }
517
+ var TCP_PROBE_TIMEOUT_MS = 3e3;
518
+ function parseRelayUrl(url) {
519
+ try {
520
+ const parsed = new URL(url);
521
+ const isSecure = parsed.protocol === "wss:" || parsed.protocol === "https:";
522
+ const defaultPort = isSecure ? 443 : 80;
523
+ const port = parsed.port.length > 0 ? parseInt(parsed.port, 10) : defaultPort;
524
+ if (!Number.isFinite(port)) {
525
+ return null;
526
+ }
527
+ return { host: parsed.hostname, port };
528
+ } catch {
529
+ return null;
530
+ }
531
+ }
532
+ async function tcpProbe(host, port, timeoutMs) {
533
+ return new Promise((resolve3) => {
534
+ const started = Date.now();
535
+ const socket = new Socket();
536
+ let settled = false;
537
+ const finish = (result) => {
538
+ if (settled) {
539
+ return;
540
+ }
541
+ settled = true;
542
+ socket.destroy();
543
+ resolve3(result);
544
+ };
545
+ const timer = setTimeout(() => finish({ error: "timeout" }), timeoutMs);
546
+ socket.once("connect", () => {
547
+ clearTimeout(timer);
548
+ finish({ openMs: Date.now() - started });
549
+ });
550
+ socket.once("error", (err) => {
551
+ clearTimeout(timer);
552
+ finish({ error: err.message });
553
+ });
554
+ socket.connect(port, host);
555
+ });
556
+ }
557
+ async function probeRelays(relays, logger, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
558
+ const results = [];
559
+ for (const url of relays) {
560
+ const parsed = parseRelayUrl(url);
561
+ if (!parsed) {
562
+ logger.debug({ event: "net_diag_parse_failed", url }, "unparseable relay URL");
563
+ results.push({ url, host: "", port: 0, ips: [], error: "invalid URL" });
564
+ continue;
565
+ }
566
+ const { host, port } = parsed;
567
+ let ips = [];
568
+ try {
569
+ const resolved = await lookup(host, { all: true });
570
+ ips = resolved.map((entry) => entry.address);
571
+ } catch (error) {
572
+ const message = error instanceof Error ? error.message : String(error);
573
+ logger.debug(
574
+ { event: "net_diag_dns_failed", url, host, error: message },
575
+ "DNS lookup failed"
576
+ );
577
+ results.push({ url, host, port, ips: [], error: `dns: ${message}` });
578
+ continue;
579
+ }
580
+ const probe = await tcpProbe(host, port, timeoutMs);
581
+ if ("openMs" in probe) {
582
+ logger.debug(
583
+ { event: "net_diag_tcp_open", url, host, port, ips, tcpOpenMs: probe.openMs },
584
+ "TCP open"
585
+ );
586
+ results.push({ url, host, port, ips, tcpOpenMs: probe.openMs });
587
+ } else {
588
+ logger.debug(
589
+ { event: "net_diag_tcp_failed", url, host, port, ips, error: probe.error },
590
+ "TCP probe failed"
591
+ );
592
+ results.push({ url, host, port, ips, error: `tcp: ${probe.error}` });
593
+ }
594
+ }
595
+ return results;
596
+ }
531
597
  var HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
532
598
  var MAX_CONCURRENT_JOBS = 10;
533
599
  var RECOVERY_MAX_RETRIES = 5;
@@ -552,8 +618,13 @@ var VALID_TRANSITIONS = {
552
618
  var JobLedger = class {
553
619
  entries = /* @__PURE__ */ new Map();
554
620
  path;
555
- constructor(agentName) {
556
- this.path = join(homedir(), ".elisym", "agents", agentName, "jobs.json");
621
+ /**
622
+ * @param ledgerPath absolute path to the ledger file
623
+ * (typically `<agentDir>/.jobs.json`). Caller is responsible for
624
+ * resolving the agent directory via `@elisym/sdk/agent-store`.
625
+ */
626
+ constructor(ledgerPath) {
627
+ this.path = ledgerPath;
557
628
  this.load();
558
629
  }
559
630
  load() {
@@ -651,13 +722,29 @@ var JobLedger = class {
651
722
  }
652
723
  /** Remove old delivered/failed entries (default: 7 days). */
653
724
  gc(maxAgeSecs = 7 * 24 * 60 * 60) {
654
- const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSecs;
725
+ this.pruneOldEntries(maxAgeSecs * 1e3);
726
+ }
727
+ /**
728
+ * Drop terminal entries (`delivered` / `failed`) whose `created_at`
729
+ * predates `now - retentionMs`. Stuck non-terminal entries are never
730
+ * pruned - recovery keeps retrying them until the retry budget runs
731
+ * out, then they become terminal and are eligible on the next sweep.
732
+ *
733
+ * Returns the number of entries deleted, for observability.
734
+ */
735
+ pruneOldEntries(retentionMs) {
736
+ const cutoff = Math.floor(Date.now() / 1e3) - Math.floor(retentionMs / 1e3);
737
+ let deleted = 0;
655
738
  for (const [id, entry] of this.entries) {
656
739
  if ((entry.status === "delivered" || entry.status === "failed") && entry.created_at < cutoff) {
657
740
  this.entries.delete(id);
741
+ deleted += 1;
658
742
  }
659
743
  }
660
- this.flush();
744
+ if (deleted > 0) {
745
+ this.flush();
746
+ }
747
+ return deleted;
661
748
  }
662
749
  };
663
750
 
@@ -677,7 +764,7 @@ function sleepWithSignal(ms, signal) {
677
764
  if (!signal) {
678
765
  return new Promise((r) => setTimeout(r, ms));
679
766
  }
680
- return new Promise((resolve, reject) => {
767
+ return new Promise((resolve3, reject) => {
681
768
  const cleanup = () => {
682
769
  clearTimeout(timer);
683
770
  signal.removeEventListener("abort", onAbort);
@@ -688,7 +775,7 @@ function sleepWithSignal(ms, signal) {
688
775
  };
689
776
  const timer = setTimeout(() => {
690
777
  cleanup();
691
- resolve();
778
+ resolve3();
692
779
  }, ms);
693
780
  signal.addEventListener("abort", onAbort, { once: true });
694
781
  });
@@ -953,8 +1040,56 @@ var OpenAIClient = class {
953
1040
  }));
954
1041
  }
955
1042
  };
1043
+ function resolveLevel(options) {
1044
+ if (options.level) {
1045
+ return options.level;
1046
+ }
1047
+ if (options.verbose) {
1048
+ return "debug";
1049
+ }
1050
+ const envLevel = process.env.LOG_LEVEL;
1051
+ if (envLevel) {
1052
+ return envLevel;
1053
+ }
1054
+ return "info";
1055
+ }
1056
+ function createLogger(options = {}) {
1057
+ const level = resolveLevel(options);
1058
+ const baseOptions = {
1059
+ name: "elisym-cli",
1060
+ level,
1061
+ redact: {
1062
+ paths: DEFAULT_REDACT_PATHS,
1063
+ censor: makeCensor()
1064
+ }
1065
+ };
1066
+ let logger;
1067
+ if (options.destination) {
1068
+ logger = pino(baseOptions, options.destination);
1069
+ } else if (options.tty === true) {
1070
+ logger = pino({
1071
+ ...baseOptions,
1072
+ transport: {
1073
+ target: "pino-pretty",
1074
+ options: { destination: 2, colorize: true, translateTime: "HH:MM:ss" }
1075
+ }
1076
+ });
1077
+ } else {
1078
+ logger = pino(baseOptions, pino.destination(2));
1079
+ }
1080
+ function logWithIndent(line) {
1081
+ process.stdout.write(` ${line}
1082
+ `);
1083
+ }
1084
+ return {
1085
+ logger,
1086
+ logWithIndent,
1087
+ bannerLog: logWithIndent
1088
+ };
1089
+ }
956
1090
  var payment = new SolanaPaymentStrategy();
957
1091
  var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
1092
+ var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
958
1093
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
959
1094
  function resolveJobPrice(tags, skills) {
960
1095
  const skill = skills.route(tags);
@@ -963,6 +1098,8 @@ function resolveJobPrice(tags, skills) {
963
1098
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
964
1099
  var MAX_JOBS_PER_CUSTOMER = 20;
965
1100
  var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
1101
+ var MAX_TRACKED_CUSTOMERS = 1e3;
1102
+ var GLOBAL_LIMITER_KEY = "__global__";
966
1103
  var AgentRuntime = class {
967
1104
  constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
968
1105
  this.transport = transport;
@@ -983,10 +1120,18 @@ var AgentRuntime = class {
983
1120
  recoveryInterval = null;
984
1121
  gcInterval = null;
985
1122
  stopped = false;
986
- /** Per-customer sliding window rate limiter: pubkey -> timestamps. */
987
- customerJobs = /* @__PURE__ */ new Map();
988
- /** Global sliding window rate limiter (Sybil protection). */
989
- globalJobs = [];
1123
+ /** Per-customer sliding-window rate limiter (keyed on customer pubkey). */
1124
+ customerLimiter = createSlidingWindowLimiter({
1125
+ windowMs: RATE_LIMIT_WINDOW_MS,
1126
+ maxPerWindow: MAX_JOBS_PER_CUSTOMER,
1127
+ maxKeys: MAX_TRACKED_CUSTOMERS
1128
+ });
1129
+ /** Global sliding-window rate limiter (Sybil protection). */
1130
+ globalLimiter = createSlidingWindowLimiter({
1131
+ windowMs: RATE_LIMIT_WINDOW_MS,
1132
+ maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
1133
+ maxKeys: 1
1134
+ });
990
1135
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
991
1136
  async fetchProtocolConfig() {
992
1137
  if (this.config.network !== "devnet") {
@@ -1001,7 +1146,7 @@ var AgentRuntime = class {
1001
1146
  }
1002
1147
  async run() {
1003
1148
  const log = this.callbacks.onLog ?? console.log;
1004
- this.ledger.gc();
1149
+ this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
1005
1150
  await this.recoverPendingJobs();
1006
1151
  this.recoveryInterval = setInterval(
1007
1152
  () => this.recoverPendingJobs().catch((e) => log(`Recovery error: ${e}`)),
@@ -1009,7 +1154,7 @@ var AgentRuntime = class {
1009
1154
  );
1010
1155
  this.gcInterval = setInterval(() => {
1011
1156
  try {
1012
- this.ledger.gc();
1157
+ this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
1013
1158
  } catch (e) {
1014
1159
  log(`GC error: ${e.message}`);
1015
1160
  }
@@ -1033,17 +1178,18 @@ var AgentRuntime = class {
1033
1178
  });
1034
1179
  return;
1035
1180
  }
1036
- if (this.isGlobalRateLimited()) {
1037
- this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
1181
+ if (!this.customerLimiter.peek(job.customerId).allowed) {
1182
+ this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
1038
1183
  });
1039
1184
  return;
1040
1185
  }
1041
- if (this.isCustomerRateLimited(job.customerId)) {
1042
- this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
1186
+ if (!this.globalLimiter.peek(GLOBAL_LIMITER_KEY).allowed) {
1187
+ this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
1043
1188
  });
1044
1189
  return;
1045
1190
  }
1046
- this.recordJobAccepted(job.customerId);
1191
+ this.customerLimiter.check(job.customerId);
1192
+ this.globalLimiter.check(GLOBAL_LIMITER_KEY);
1047
1193
  this.callbacks.onJobReceived?.(job);
1048
1194
  this.inFlight.add(job.jobId);
1049
1195
  this.pending++;
@@ -1055,8 +1201,8 @@ var AgentRuntime = class {
1055
1201
  });
1056
1202
  });
1057
1203
  log("Agent runtime started. Listening for jobs...");
1058
- await new Promise((resolve) => {
1059
- this.abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1204
+ await new Promise((resolve3) => {
1205
+ this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
1060
1206
  process.on("SIGINT", () => {
1061
1207
  log("Shutting down...");
1062
1208
  this.stop();
@@ -1069,40 +1215,10 @@ var AgentRuntime = class {
1069
1215
  });
1070
1216
  });
1071
1217
  }
1072
- /** Returns true if global job throughput has been exceeded (Sybil protection). Pure check - no side effects. */
1073
- isGlobalRateLimited() {
1074
- const now = Date.now();
1075
- this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1076
- return this.globalJobs.length >= GLOBAL_MAX_JOBS_PER_WINDOW;
1077
- }
1078
- /** Returns true if customer has exceeded the per-window job limit. Pure check - no side effects. */
1079
- isCustomerRateLimited(customerId) {
1080
- const now = Date.now();
1081
- const timestamps = this.customerJobs.get(customerId) ?? [];
1082
- const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1083
- this.customerJobs.set(customerId, recent);
1084
- return recent.length >= MAX_JOBS_PER_CUSTOMER;
1085
- }
1086
- /** Record a job acceptance for rate limiting. Called only after all checks pass. */
1087
- recordJobAccepted(customerId) {
1088
- const now = Date.now();
1089
- this.globalJobs.push(now);
1090
- const timestamps = this.customerJobs.get(customerId) ?? [];
1091
- timestamps.push(now);
1092
- this.customerJobs.set(customerId, timestamps);
1093
- }
1094
- /** Clean up expired rate limit entries. */
1218
+ /** Drop expired hits from both sliding-window limiters. */
1095
1219
  cleanupRateLimits() {
1096
- const now = Date.now();
1097
- this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1098
- for (const [key, timestamps] of this.customerJobs) {
1099
- const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1100
- if (recent.length === 0) {
1101
- this.customerJobs.delete(key);
1102
- } else {
1103
- this.customerJobs.set(key, recent);
1104
- }
1105
- }
1220
+ this.customerLimiter.prune();
1221
+ this.globalLimiter.prune();
1106
1222
  }
1107
1223
  stop() {
1108
1224
  if (this.stopped) {
@@ -1479,7 +1595,6 @@ var SkillRegistry = class {
1479
1595
  return this.skills;
1480
1596
  }
1481
1597
  };
1482
- var MAX_TOOL_OUTPUT = 1e6;
1483
1598
  var ScriptSkill = class {
1484
1599
  name;
1485
1600
  description;
@@ -1487,10 +1602,7 @@ var ScriptSkill = class {
1487
1602
  priceLamports;
1488
1603
  image;
1489
1604
  imageFile;
1490
- skillDir;
1491
- systemPrompt;
1492
- tools;
1493
- maxToolRounds;
1605
+ inner;
1494
1606
  constructor(name, description, capabilities, priceLamports, image, imageFile, skillDir, systemPrompt, tools, maxToolRounds) {
1495
1607
  this.name = name;
1496
1608
  this.description = description;
@@ -1498,155 +1610,25 @@ var ScriptSkill = class {
1498
1610
  this.priceLamports = priceLamports;
1499
1611
  this.image = image;
1500
1612
  this.imageFile = imageFile;
1501
- this.skillDir = skillDir;
1502
- this.systemPrompt = systemPrompt;
1503
- this.tools = tools;
1504
- this.maxToolRounds = maxToolRounds;
1613
+ this.inner = new ScriptSkill$1({
1614
+ name,
1615
+ description,
1616
+ capabilities,
1617
+ priceLamports: BigInt(Math.round(priceLamports)),
1618
+ skillDir,
1619
+ systemPrompt,
1620
+ tools,
1621
+ maxToolRounds,
1622
+ image,
1623
+ imageFile
1624
+ });
1505
1625
  }
1506
1626
  async execute(input, ctx) {
1507
- if (!ctx.llm) {
1508
- throw new Error("LLM client not configured");
1509
- }
1510
- if (this.tools.length === 0) {
1511
- const result = await ctx.llm.complete(this.systemPrompt, input.data, ctx.signal);
1512
- return { data: result };
1513
- }
1514
- const toolDefs = this.tools.map((t) => ({
1515
- name: t.name,
1516
- description: t.description,
1517
- parameters: (t.parameters ?? []).map((p) => ({
1518
- name: p.name,
1519
- description: p.description,
1520
- required: p.required ?? true
1521
- }))
1522
- }));
1523
- const messages = [{ role: "user", content: input.data }];
1524
- for (let round = 0; round < this.maxToolRounds; round++) {
1525
- if (ctx.signal?.aborted) {
1526
- throw new Error("Job aborted");
1527
- }
1528
- const result = await ctx.llm.completeWithTools(
1529
- this.systemPrompt,
1530
- messages,
1531
- toolDefs,
1532
- ctx.signal
1533
- );
1534
- if (result.type === "text") {
1535
- return { data: result.text };
1536
- }
1537
- messages.push(result.assistantMessage);
1538
- const toolResults = [];
1539
- for (const call of result.calls) {
1540
- const toolDef = this.tools.find((t) => t.name === call.name);
1541
- if (!toolDef) {
1542
- toolResults.push({
1543
- callId: call.id,
1544
- content: `Error: unknown tool "${call.name}"`
1545
- });
1546
- continue;
1547
- }
1548
- const output = await this.runTool(toolDef, call, ctx.signal);
1549
- toolResults.push({ callId: call.id, content: output });
1550
- }
1551
- messages.push(...ctx.llm.formatToolResultMessages(toolResults));
1552
- }
1553
- throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
1554
- }
1555
- runTool(toolDef, call, signal) {
1556
- return new Promise((resolve, _reject) => {
1557
- const args = [...toolDef.command];
1558
- const cmd = args.shift();
1559
- const params = toolDef.parameters ?? [];
1560
- for (const param of params) {
1561
- const value = call.arguments[param.name];
1562
- if (value !== void 0) {
1563
- if (param.required && params.indexOf(param) === 0) {
1564
- args.push(String(value));
1565
- } else {
1566
- args.push(`--${param.name}`, String(value));
1567
- }
1568
- }
1569
- }
1570
- const child = spawn(cmd, args, {
1571
- cwd: this.skillDir,
1572
- stdio: ["pipe", "pipe", "pipe"],
1573
- timeout: 6e4,
1574
- killSignal: "SIGKILL",
1575
- signal
1576
- });
1577
- let stdout = "";
1578
- let stderr = "";
1579
- const stdoutDecoder = new StringDecoder("utf8");
1580
- const stderrDecoder = new StringDecoder("utf8");
1581
- child.stdout.on("data", (data) => {
1582
- if (stdout.length < MAX_TOOL_OUTPUT) {
1583
- stdout += stdoutDecoder.write(data);
1584
- if (stdout.length > MAX_TOOL_OUTPUT) {
1585
- stdout = stdout.slice(0, MAX_TOOL_OUTPUT);
1586
- }
1587
- }
1588
- });
1589
- child.stderr.on("data", (data) => {
1590
- if (stderr.length < MAX_TOOL_OUTPUT) {
1591
- stderr += stderrDecoder.write(data);
1592
- if (stderr.length > MAX_TOOL_OUTPUT) {
1593
- stderr = stderr.slice(0, MAX_TOOL_OUTPUT);
1594
- }
1595
- }
1596
- });
1597
- child.on("close", (code) => {
1598
- if (code === 0) {
1599
- resolve(stdout.trim());
1600
- } else {
1601
- resolve(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1602
- }
1603
- });
1604
- child.on("error", (err) => {
1605
- resolve(`Error: ${err.message}`);
1606
- });
1607
- });
1627
+ return this.inner.execute(input, ctx);
1608
1628
  }
1609
1629
  };
1610
1630
 
1611
1631
  // src/skill/loader.ts
1612
- var LAMPORTS_PER_SOL = 1e9;
1613
- function solToLamports(sol) {
1614
- if (!Number.isFinite(sol) || sol < 0) {
1615
- return 0;
1616
- }
1617
- return Math.round(sol * LAMPORTS_PER_SOL);
1618
- }
1619
- function parseSkillMd(content) {
1620
- const lines = content.split("\n");
1621
- let start = -1;
1622
- let end = -1;
1623
- for (let i = 0; i < lines.length; i++) {
1624
- if (lines[i].trim() === "---") {
1625
- if (start === -1) {
1626
- start = i;
1627
- } else {
1628
- end = i;
1629
- break;
1630
- }
1631
- }
1632
- }
1633
- if (start === -1 || end === -1) {
1634
- throw new Error("SKILL.md must have YAML frontmatter between --- delimiters");
1635
- }
1636
- const yamlStr = lines.slice(start + 1, end).join("\n");
1637
- const frontmatter = YAML.parse(yamlStr);
1638
- if (!frontmatter.name || typeof frontmatter.name !== "string") {
1639
- throw new Error('SKILL.md: missing or invalid "name" field');
1640
- }
1641
- if (!frontmatter.description || typeof frontmatter.description !== "string") {
1642
- throw new Error('SKILL.md: missing or invalid "description" field');
1643
- }
1644
- if (!Array.isArray(frontmatter.capabilities) || frontmatter.capabilities.length === 0) {
1645
- throw new Error('SKILL.md: "capabilities" must be a non-empty array');
1646
- }
1647
- const systemPrompt = lines.slice(end + 1).join("\n").trim();
1648
- return { frontmatter, systemPrompt };
1649
- }
1650
1632
  function loadSkillsFromDir(skillsDir) {
1651
1633
  const skills = [];
1652
1634
  let entries;
@@ -1668,22 +1650,26 @@ function loadSkillsFromDir(skillsDir) {
1668
1650
  try {
1669
1651
  const content = readFileSync(skillMdPath, "utf-8");
1670
1652
  const { frontmatter, systemPrompt } = parseSkillMd(content);
1653
+ const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
1654
+ allowFreeSkills: true
1655
+ });
1671
1656
  skills.push(
1672
1657
  new ScriptSkill(
1673
- frontmatter.name,
1674
- frontmatter.description,
1675
- frontmatter.capabilities,
1676
- solToLamports(frontmatter.price ?? 0),
1677
- frontmatter.image,
1678
- frontmatter.image_file,
1658
+ parsed.name,
1659
+ parsed.description,
1660
+ parsed.capabilities,
1661
+ Number(parsed.priceLamports),
1662
+ parsed.image,
1663
+ parsed.imageFile,
1679
1664
  entryPath,
1680
- systemPrompt,
1681
- frontmatter.tools ?? [],
1682
- frontmatter.max_tool_rounds ?? 10
1665
+ parsed.systemPrompt,
1666
+ parsed.tools,
1667
+ parsed.maxToolRounds
1683
1668
  )
1684
1669
  );
1685
1670
  } catch (e) {
1686
- console.warn(` ! Skipping skill "${entry}": ${e?.message ?? "unknown error"}`);
1671
+ const message = e instanceof Error ? e.message : String(e);
1672
+ console.warn(` ! Skipping skill "${entry}": ${message}`);
1687
1673
  }
1688
1674
  }
1689
1675
  return skills;
@@ -1802,6 +1788,7 @@ function startWatchdog(deps) {
1802
1788
  transport,
1803
1789
  onPing,
1804
1790
  log,
1791
+ logger,
1805
1792
  probeIntervalMs = WATCHDOG_PROBE_INTERVAL_MS,
1806
1793
  probeTimeoutMs = WATCHDOG_PROBE_TIMEOUT_MS,
1807
1794
  selfPingIntervalMs = WATCHDOG_SELF_PING_INTERVAL_MS,
@@ -1827,10 +1814,12 @@ function startWatchdog(deps) {
1827
1814
  return;
1828
1815
  }
1829
1816
  log("[watchdog] relay probe failed, resetting pool and re-subscribing");
1817
+ logger?.info({ event: "pool_reset", reason: "probe_failed" }, "pool reset");
1830
1818
  resetPoolAndResubscribe();
1831
1819
  } catch (error) {
1832
1820
  const errorMessage = error instanceof Error ? error.message : String(error);
1833
1821
  log(`[watchdog] probe/reset error: ${errorMessage}`);
1822
+ logger?.warn({ event: "probe_error", error: errorMessage }, "watchdog probe/reset error");
1834
1823
  } finally {
1835
1824
  busy = false;
1836
1825
  }
@@ -1846,10 +1835,15 @@ function startWatchdog(deps) {
1846
1835
  return;
1847
1836
  }
1848
1837
  log("[watchdog] self-ping failed, resetting pool and re-subscribing");
1838
+ logger?.info({ event: "pool_reset", reason: "self_ping_failed" }, "pool reset");
1849
1839
  resetPoolAndResubscribe();
1850
1840
  } catch (error) {
1851
1841
  const errorMessage = error instanceof Error ? error.message : String(error);
1852
1842
  log(`[watchdog] self-ping/reset error: ${errorMessage}`);
1843
+ logger?.warn(
1844
+ { event: "self_ping_error", error: errorMessage },
1845
+ "watchdog self-ping/reset error"
1846
+ );
1853
1847
  } finally {
1854
1848
  busy = false;
1855
1849
  }
@@ -1868,37 +1862,33 @@ function startWatchdog(deps) {
1868
1862
  }
1869
1863
 
1870
1864
  // src/commands/start.ts
1871
- async function cmdStart(name) {
1872
- if (!name) {
1873
- const agents = listAgents();
1874
- if (agents.length === 0) {
1875
- console.log("No agents configured. Run `elisym init` first.");
1876
- process.exit(1);
1877
- }
1878
- const { default: inquirer } = await import('inquirer');
1879
- const choices = [...agents, { name: "+ Create new agent", value: "__new__" }];
1880
- const { selected } = await inquirer.prompt([
1881
- { type: "list", name: "selected", message: "Select agent to start", choices }
1882
- ]);
1883
- if (selected === "__new__") {
1884
- const { cmdInit: cmdInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1885
- await cmdInit2();
1886
- return;
1887
- }
1888
- name = selected;
1865
+ async function cmdStart(nameArg, options = {}) {
1866
+ const cwd = process.cwd();
1867
+ const agentName = await resolveStartAgentName(nameArg, cwd);
1868
+ if (!agentName) {
1869
+ return;
1889
1870
  }
1890
- const config = await loadConfigWithPrompt(name);
1871
+ const loaded = await loadAgentWithPrompt(agentName, cwd);
1872
+ const verbose = options.verbose === true || process.env.ELISYM_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
1873
+ const { logger, logWithIndent } = createLogger({
1874
+ verbose,
1875
+ tty: Boolean(process.stdout.isTTY)
1876
+ });
1891
1877
  console.log(`
1892
- Starting agent ${name}...
1878
+ Starting agent ${agentName} (${loaded.source})...
1893
1879
  `);
1894
- let solanaAddress;
1895
- if (config.payments?.length) {
1896
- const solPayment = config.payments.find((p) => p.chain === "solana");
1897
- if (solPayment) {
1898
- solanaAddress = solPayment.address;
1899
- }
1880
+ if (loaded.shadowsGlobal) {
1881
+ console.log(
1882
+ ` ! Using project-local ${agentName} (shadows global agent in ~/.elisym/${agentName}/)
1883
+ `
1884
+ );
1885
+ }
1886
+ if (verbose) {
1887
+ console.log(" [debug] Verbose logging enabled. Structured diagnostics -> stderr.");
1900
1888
  }
1901
- const walletNetwork = config.payments?.find((p) => p.chain === "solana")?.network ?? "devnet";
1889
+ const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
1890
+ const solanaAddress = solPayment?.address;
1891
+ const walletNetwork = solPayment?.network ?? "devnet";
1902
1892
  if (solanaAddress) {
1903
1893
  try {
1904
1894
  const rpcUrl = getRpcUrl(walletNetwork);
@@ -1922,17 +1912,26 @@ async function cmdStart(name) {
1922
1912
  `);
1923
1913
  }
1924
1914
  }
1925
- if (!config.llm) {
1915
+ if (!loaded.yaml.llm) {
1926
1916
  console.error(" ! No LLM configured. Run `elisym init` to set up LLM.\n");
1927
1917
  process.exit(1);
1928
1918
  }
1929
- const skillsDir = join(process.cwd(), "skills");
1919
+ if (!loaded.secrets.llm_api_key) {
1920
+ console.error(
1921
+ ` ! No LLM API key. Set ${loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} env var or update the agent's secrets.
1922
+ `
1923
+ );
1924
+ process.exit(1);
1925
+ }
1926
+ const paths = agentPaths(loaded.dir);
1927
+ const skillsDir = paths.skills;
1930
1928
  const allSkills = loadSkillsFromDir(skillsDir);
1931
1929
  if (allSkills.length === 0) {
1932
1930
  console.error(` ! No skills found in ${skillsDir}
1933
1931
  `);
1934
1932
  console.error(" Create a skill directory with a SKILL.md to get started.");
1935
- console.error(" Example: ./skills/my-skill/SKILL.md\n");
1933
+ console.error(` Example: ${skillsDir}/my-skill/SKILL.md
1934
+ `);
1936
1935
  process.exit(1);
1937
1936
  }
1938
1937
  const registry = new SkillRegistry();
@@ -1948,53 +1947,88 @@ async function cmdStart(name) {
1948
1947
  process.exit(1);
1949
1948
  }
1950
1949
  const llm = createLlmClient({
1951
- provider: config.llm.provider,
1952
- apiKey: config.llm.api_key,
1953
- model: config.llm.model,
1954
- maxTokens: config.llm.max_tokens,
1950
+ provider: loaded.yaml.llm.provider,
1951
+ apiKey: loaded.secrets.llm_api_key,
1952
+ model: loaded.yaml.llm.model,
1953
+ maxTokens: loaded.yaml.llm.max_tokens,
1955
1954
  logUsage: true
1956
1955
  });
1957
1956
  const skillCtx = {
1958
1957
  llm,
1959
- agentName: config.identity.name,
1960
- agentDescription: config.identity.description ?? ""
1958
+ agentName,
1959
+ agentDescription: loaded.yaml.description ?? ""
1961
1960
  };
1962
1961
  console.log(" Connecting to relays and publishing capabilities...");
1963
- const identity = ElisymIdentity.fromHex(config.identity.secret_key);
1964
- const relays = config.relays?.length ? config.relays : [...RELAYS];
1962
+ const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
1963
+ const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
1965
1964
  const client = new ElisymClient({ relays });
1965
+ if (process.env.ELISYM_NET_DIAG === "1") {
1966
+ console.log(" [net-diag] Probing relay DNS + TCP connectivity...");
1967
+ const results = await probeRelays(relays, logger);
1968
+ for (const result of results) {
1969
+ const ipSummary = result.ips.length > 0 ? result.ips.join(",") : "-";
1970
+ if (result.tcpOpenMs !== void 0) {
1971
+ console.log(` [net-diag] ${result.url} -> ${ipSummary} TCP open in ${result.tcpOpenMs}ms`);
1972
+ } else {
1973
+ console.log(` [net-diag] ${result.url} -> ${ipSummary} FAILED: ${result.error ?? "?"}`);
1974
+ }
1975
+ }
1976
+ }
1966
1977
  const media = new MediaService();
1978
+ const mediaCache = await readMediaCache(loaded.dir);
1979
+ let mediaCacheDirty = false;
1980
+ const pictureUrl = await resolveMediaField(
1981
+ loaded.yaml.picture,
1982
+ loaded.dir,
1983
+ mediaCache,
1984
+ media,
1985
+ identity,
1986
+ (updated) => mediaCacheDirty = mediaCacheDirty || updated
1987
+ );
1988
+ const bannerUrl = await resolveMediaField(
1989
+ loaded.yaml.banner,
1990
+ loaded.dir,
1991
+ mediaCache,
1992
+ media,
1993
+ identity,
1994
+ (updated) => mediaCacheDirty = mediaCacheDirty || updated
1995
+ );
1967
1996
  for (const skill of allSkills) {
1968
1997
  if (skill.image || !skill.imageFile) {
1969
1998
  continue;
1970
1999
  }
1971
- try {
1972
- const filePath = join(skillsDir, skill.name, skill.imageFile);
1973
- console.log(` Uploading ${basename(filePath)}...`);
1974
- const data = readFileSync(filePath);
1975
- const blob = new Blob([data]);
1976
- const url = await media.upload(identity, blob, basename(filePath));
1977
- console.log(` Uploaded: ${url}`);
2000
+ const skillRoot = join(skillsDir, skill.name);
2001
+ const cacheKey = `./skills/${skill.name}/${skill.imageFile}`;
2002
+ const absPath = join(skillRoot, skill.imageFile);
2003
+ const url = await uploadOrReuse(
2004
+ cacheKey,
2005
+ absPath,
2006
+ mediaCache,
2007
+ media,
2008
+ identity,
2009
+ () => mediaCacheDirty = true
2010
+ );
2011
+ if (url) {
1978
2012
  skill.image = url;
1979
- const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
1980
- const mdContent = readFileSync(skillMdPath, "utf-8");
1981
- const updated = mdContent.replace(/^(image_file:\s*.+)$/m, (m) => `${m}
1982
- image: ${url}`);
1983
- writeFileSync(skillMdPath, updated);
1984
- } catch (e) {
1985
- console.warn(` ! Failed to upload image for "${skill.name}": ${e.message}`);
1986
2013
  }
1987
2014
  }
2015
+ if (mediaCacheDirty) {
2016
+ await writeMediaCache(loaded.dir, mediaCache);
2017
+ }
2018
+ let profilePublished = false;
1988
2019
  try {
1989
2020
  await client.discovery.publishProfile(
1990
2021
  identity,
1991
- config.identity.name,
1992
- config.identity.description ?? "",
1993
- config.identity.picture,
1994
- config.identity.banner
2022
+ agentName,
2023
+ loaded.yaml.description ?? "",
2024
+ pictureUrl,
2025
+ bannerUrl
1995
2026
  );
2027
+ profilePublished = true;
2028
+ logger.debug({ event: "publish_ack", kind: 0 }, "profile published");
1996
2029
  } catch (e) {
1997
2030
  console.warn(` ! Failed to publish profile: ${e.message}`);
2031
+ logger.warn({ event: "publish_failed", kind: 0, error: e.message }, "profile publish failed");
1998
2032
  }
1999
2033
  const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
2000
2034
  function buildCard(skill) {
@@ -2011,11 +2045,21 @@ image: ${url}`);
2011
2045
  } : void 0
2012
2046
  };
2013
2047
  }
2048
+ let cardsPublished = 0;
2014
2049
  for (const skill of allSkills) {
2015
2050
  try {
2016
2051
  await client.discovery.publishCapability(identity, buildCard(skill), kinds);
2052
+ cardsPublished += 1;
2053
+ logger.debug(
2054
+ { event: "publish_ack", kind: 31990, skill: skill.name },
2055
+ "capability card published"
2056
+ );
2017
2057
  } catch (e) {
2018
2058
  console.warn(` ! Failed to publish "${skill.name}": ${e.message}`);
2059
+ logger.warn(
2060
+ { event: "publish_failed", kind: 31990, skill: skill.name, error: e.message },
2061
+ "capability publish failed"
2062
+ );
2019
2063
  }
2020
2064
  }
2021
2065
  try {
@@ -2042,6 +2086,12 @@ image: ${url}`);
2042
2086
  } catch {
2043
2087
  }
2044
2088
  console.log(" Connected.\n");
2089
+ if (verbose) {
2090
+ console.log(
2091
+ ` [debug] Published: profile=${profilePublished ? "ok" : "FAILED"} + ${cardsPublished}/${allSkills.length} capability cards (kind:31990)`
2092
+ );
2093
+ console.log(` [debug] Relays: ${relays.length} configured (${relays.join(", ")})`);
2094
+ }
2045
2095
  const onPing = (senderPubkey, nonce) => {
2046
2096
  client.ping.sendPong(identity, senderPubkey, nonce).catch(() => {
2047
2097
  });
@@ -2052,12 +2102,15 @@ image: ${url}`);
2052
2102
  heartbeatTimer = setInterval(async () => {
2053
2103
  try {
2054
2104
  await client.discovery.publishCapability(identity, heartbeatCard, kinds);
2055
- } catch {
2105
+ logger.debug({ event: "heartbeat_ack", skill: heartbeatCard.name }, "heartbeat ok");
2106
+ } catch (error) {
2107
+ const message = error instanceof Error ? error.message : String(error);
2108
+ logger.warn({ event: "heartbeat_failed", error: message }, "heartbeat publish failed");
2056
2109
  }
2057
2110
  }, HEARTBEAT_INTERVAL_MS);
2058
2111
  }
2059
2112
  const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
2060
- const ledger = new JobLedger(name);
2113
+ const ledger = new JobLedger(paths.jobs);
2061
2114
  const runtimeConfig = {
2062
2115
  paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
2063
2116
  maxConcurrentJobs: MAX_CONCURRENT_JOBS,
@@ -2066,26 +2119,49 @@ image: ${url}`);
2066
2119
  network: walletNetwork,
2067
2120
  solanaAddress
2068
2121
  };
2069
- const logWithIndent = (msg) => console.log(` ${msg}`);
2122
+ const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
2123
+ logger.debug(
2124
+ {
2125
+ event: "config_resolved",
2126
+ agent: agentName,
2127
+ source: loaded.source,
2128
+ network: walletNetwork,
2129
+ relays,
2130
+ solanaAddress,
2131
+ rpcUrl: rpcUrlForLog
2132
+ },
2133
+ "config resolved"
2134
+ );
2135
+ const diagLog = (msg) => {
2136
+ logWithIndent(msg);
2137
+ logger.info({ event: "runtime_diag" }, msg);
2138
+ };
2070
2139
  const watchdog = startWatchdog({
2071
2140
  client,
2072
2141
  identity,
2073
2142
  transport,
2074
2143
  onPing,
2075
- log: logWithIndent
2144
+ log: diagLog,
2145
+ logger
2076
2146
  });
2077
2147
  const runtime = new AgentRuntime(transport, registry, skillCtx, runtimeConfig, ledger, {
2078
2148
  onJobReceived: (job) => {
2079
2149
  const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
2080
- console.log(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}`);
2150
+ process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
2151
+ `);
2152
+ logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
2081
2153
  },
2082
2154
  onJobCompleted: (jobId) => {
2083
- console.log(` [job] ${jobId.slice(0, 16)} | delivered`);
2155
+ process.stdout.write(` [job] ${jobId.slice(0, 16)} | delivered
2156
+ `);
2157
+ logger.info({ event: "job_delivered", jobId });
2084
2158
  },
2085
2159
  onJobError: (jobId, error) => {
2086
- console.error(` [job] ${jobId.slice(0, 16)} | error: ${error}`);
2160
+ process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
2161
+ `);
2162
+ logger.error({ event: "job_error", jobId, error });
2087
2163
  },
2088
- onLog: logWithIndent,
2164
+ onLog: diagLog,
2089
2165
  onStop: () => {
2090
2166
  watchdog.stop();
2091
2167
  if (heartbeatTimer) {
@@ -2096,15 +2172,100 @@ image: ${url}`);
2096
2172
  console.log(" * Running. Press Ctrl+C to stop.\n");
2097
2173
  await runtime.run();
2098
2174
  }
2175
+ function stripRpcSecrets(raw) {
2176
+ try {
2177
+ const parsed = new URL(raw);
2178
+ parsed.username = "";
2179
+ parsed.password = "";
2180
+ const marker = parsed.search.length > 0 ? "?***" : "";
2181
+ parsed.search = "";
2182
+ return `${parsed.toString()}${marker}`;
2183
+ } catch {
2184
+ return "[unparseable RPC URL]";
2185
+ }
2186
+ }
2187
+ async function resolveMediaField(value, agentDir, cache, media, identity, onCacheUpdate) {
2188
+ if (!value) {
2189
+ return void 0;
2190
+ }
2191
+ if (isRemoteUrl(value)) {
2192
+ return value;
2193
+ }
2194
+ const absPath = resolveInsideAgentDir(value, agentDir);
2195
+ if (!absPath) {
2196
+ console.warn(` ! Skipping media field "${value}": path must stay inside the agent directory.`);
2197
+ return void 0;
2198
+ }
2199
+ return uploadOrReuse(value, absPath, cache, media, identity, () => onCacheUpdate(true));
2200
+ }
2201
+ function resolveInsideAgentDir(value, agentDir) {
2202
+ const agentRoot = resolve(agentDir);
2203
+ const candidate = resolve(agentRoot, value);
2204
+ const rel = relative(agentRoot, candidate);
2205
+ if (rel === "" || rel.startsWith("..") || rel.includes(`..${sep}`)) {
2206
+ return null;
2207
+ }
2208
+ return candidate;
2209
+ }
2210
+ async function uploadOrReuse(cacheKey, absPath, cache, media, identity, onCacheUpdate) {
2211
+ try {
2212
+ const cached = await lookupCachedUrl(cache, cacheKey, absPath);
2213
+ if (cached) {
2214
+ return cached;
2215
+ }
2216
+ console.log(` Uploading ${basename(absPath)}...`);
2217
+ const data = readFileSync(absPath);
2218
+ const sha256 = createHash("sha256").update(data).digest("hex");
2219
+ const blob = new Blob([data]);
2220
+ const url = await media.upload(identity, blob, basename(absPath));
2221
+ cache[cacheKey] = newCacheEntry(url, sha256);
2222
+ onCacheUpdate();
2223
+ console.log(` Uploaded: ${url}`);
2224
+ return url;
2225
+ } catch (e) {
2226
+ console.warn(` ! Failed to upload ${basename(absPath)}: ${e.message}`);
2227
+ return void 0;
2228
+ }
2229
+ }
2230
+ function isRemoteUrl(value) {
2231
+ return /^https?:\/\//i.test(value);
2232
+ }
2099
2233
  var MAX_PASSPHRASE_ATTEMPTS = 3;
2100
- async function loadConfigWithPrompt(name) {
2234
+ async function resolveStartAgentName(nameArg, cwd) {
2235
+ if (nameArg) {
2236
+ return nameArg;
2237
+ }
2238
+ const agents = await listAgents(cwd);
2239
+ if (agents.length === 0) {
2240
+ console.log("No agents configured. Run `elisym init` first.");
2241
+ process.exit(1);
2242
+ }
2243
+ const { default: inquirer } = await import('inquirer');
2244
+ const choices = [
2245
+ ...agents.map((agent) => ({
2246
+ name: `${agent.name} (${agent.source}${agent.shadowsGlobal ? " - shadows global" : ""})`,
2247
+ value: agent.name
2248
+ })),
2249
+ { name: "+ Create new agent", value: "__new__" }
2250
+ ];
2251
+ const { selected } = await inquirer.prompt([
2252
+ { type: "list", name: "selected", message: "Select agent to start", choices }
2253
+ ]);
2254
+ if (selected === "__new__") {
2255
+ const { cmdInit: cmdInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2256
+ await cmdInit2();
2257
+ return void 0;
2258
+ }
2259
+ return selected;
2260
+ }
2261
+ async function loadAgentWithPrompt(name, cwd) {
2101
2262
  const envPassphrase = process.env.ELISYM_PASSPHRASE;
2102
2263
  try {
2103
- return loadConfig(name, envPassphrase);
2264
+ return await loadAgent(name, cwd, envPassphrase);
2104
2265
  } catch (e) {
2105
- const isEncryptedConfigError = /encrypted but no passphrase/i.test(e?.message ?? "");
2266
+ const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2106
2267
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2107
- if (!isEncryptedConfigError && !isWrongPassphrase) {
2268
+ if (!isEncrypted3 && !isWrongPassphrase) {
2108
2269
  throw e;
2109
2270
  }
2110
2271
  if (!process.stdin.isTTY) {
@@ -2122,7 +2283,7 @@ async function loadConfigWithPrompt(name) {
2122
2283
  }
2123
2284
  ]);
2124
2285
  try {
2125
- return loadConfig(name, passphrase);
2286
+ return await loadAgent(name, cwd, passphrase);
2126
2287
  } catch (e) {
2127
2288
  if (!/Decryption failed/i.test(e?.message ?? "")) {
2128
2289
  throw e;
@@ -2137,12 +2298,10 @@ async function loadConfigWithPrompt(name) {
2137
2298
  }
2138
2299
  throw new Error("Unreachable");
2139
2300
  }
2140
-
2141
- // src/commands/wallet.ts
2142
- init_config();
2143
2301
  async function cmdWallet(name) {
2302
+ const cwd = process.cwd();
2144
2303
  if (!name) {
2145
- const agents = listAgents();
2304
+ const agents = await listAgents(cwd);
2146
2305
  if (agents.length === 0) {
2147
2306
  console.error("No agents found.");
2148
2307
  process.exit(1);
@@ -2153,33 +2312,32 @@ async function cmdWallet(name) {
2153
2312
  type: "list",
2154
2313
  name: "selected",
2155
2314
  message: "Select agent:",
2156
- choices: agents
2315
+ choices: agents.map((agent) => ({
2316
+ name: `${agent.name} (${agent.source})`,
2317
+ value: agent.name
2318
+ }))
2157
2319
  }
2158
2320
  ]);
2159
2321
  name = selected;
2160
2322
  }
2161
2323
  const passphrase = process.env.ELISYM_PASSPHRASE;
2162
- const config = loadConfig(name, passphrase);
2163
- const solPayment = config.payments?.find((p) => p.chain === "solana");
2324
+ const loaded = await loadAgent(name, cwd, passphrase);
2325
+ const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
2164
2326
  if (!solPayment?.address) {
2165
2327
  console.error("Solana address not configured for this agent.");
2166
2328
  process.exit(1);
2167
2329
  }
2168
- const network = solPayment.network ?? "devnet";
2169
- const rpcUrl = getRpcUrl();
2330
+ const rpcUrl = getRpcUrl(solPayment.network);
2170
2331
  const rpc = createSolanaRpc(rpcUrl);
2171
2332
  const walletAddress = address(solPayment.address);
2172
2333
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2173
2334
  console.log(`
2174
2335
  Agent: ${name}`);
2175
- console.log(` Network: ${network}`);
2336
+ console.log(` Network: ${solPayment.network}`);
2176
2337
  console.log(` Address: ${solPayment.address}`);
2177
2338
  console.log(` Balance: ${formatSol(Number(balance))} (${balance} lamports)
2178
2339
  `);
2179
2340
  }
2180
-
2181
- // src/index.ts
2182
- init_config();
2183
2341
  function readPackageVersion() {
2184
2342
  try {
2185
2343
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2193,50 +2351,57 @@ var PACKAGE_VERSION = readPackageVersion();
2193
2351
 
2194
2352
  // src/index.ts
2195
2353
  process.removeAllListeners("warning");
2196
- var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
2197
- program.command("init").description("Create a new agent (interactive wizard)").action(cmdInit);
2198
- program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(cmdProfile);
2199
- program.command("start [name]").description("Start agent in provider mode").action(cmdStart);
2200
- program.command("list").description("List all agents").action(() => {
2201
- const agents = listAgents();
2202
- if (agents.length === 0) {
2203
- console.log("No agents found. Run `elisym init` to create one.");
2204
- return;
2205
- }
2206
- console.log("\nAgents:");
2207
- for (const name of agents) {
2354
+ function safe(fn) {
2355
+ return async (...args) => {
2208
2356
  try {
2209
- const config = loadConfig(name);
2210
- const secretKey = config.identity.secret_key;
2211
- let npub = "(encrypted)";
2212
- if (!secretKey.startsWith("encrypted:")) {
2213
- const secretBytes = Buffer.from(secretKey, "hex");
2214
- npub = secretBytes.length === 32 ? nip19.npubEncode(getPublicKey(secretBytes)) : "(invalid key)";
2215
- }
2216
- const solAddr = config.payments?.[0]?.address ? ` | Solana: ${config.payments[0].address}` : "";
2217
- console.log(` ${name} | ${npub}${solAddr}`);
2218
- } catch {
2219
- console.log(` ${name} (error loading config)`);
2357
+ await fn(...args);
2358
+ } catch (e) {
2359
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
2360
+ process.exit(1);
2220
2361
  }
2221
- }
2222
- console.log();
2223
- });
2224
- program.command("wallet [name]").description("Show wallet balance").action(cmdWallet);
2225
- program.command("delete <name>").description("Delete an agent").action(async (name) => {
2226
- const { default: inquirer } = await import('inquirer');
2227
- const { confirm } = await inquirer.prompt([
2228
- {
2229
- type: "confirm",
2230
- name: "confirm",
2231
- message: `Delete agent "${name}"? This cannot be undone.`,
2232
- default: false
2362
+ };
2363
+ }
2364
+ var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
2365
+ program.command("init [name]").description("Create a new agent").option("-c, --config <path>", "Load fields from an elisym.yaml template (non-interactive)").option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").action(
2366
+ safe(async (name, options) => {
2367
+ await cmdInit(name, options);
2368
+ })
2369
+ );
2370
+ program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(safe(cmdProfile));
2371
+ program.command("start [name]").description("Start agent in provider mode").option(
2372
+ "-v, --verbose",
2373
+ "Enable debug logging (relay lifecycle, publish acks, subscription EOSE). Also togglable via ELISYM_DEBUG=1 or LOG_LEVEL=debug."
2374
+ ).action(
2375
+ safe(async (name, options) => {
2376
+ await cmdStart(name, options);
2377
+ })
2378
+ );
2379
+ program.command("list").description("List all agents (project-local and home-global)").action(
2380
+ safe(async () => {
2381
+ const cwd = process.cwd();
2382
+ const agents = await listAgents(cwd);
2383
+ if (agents.length === 0) {
2384
+ console.log("No agents found. Run `elisym init` to create one.");
2385
+ return;
2233
2386
  }
2234
- ]);
2235
- if (confirm) {
2236
- deleteAgent(name);
2237
- console.log(`Agent "${name}" deleted.`);
2238
- }
2239
- });
2387
+ console.log("\nAgents:");
2388
+ for (const agent of agents) {
2389
+ try {
2390
+ const loaded = await loadAgent(agent.name, cwd);
2391
+ const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
2392
+ const npub = nip19.npubEncode(identity.publicKey);
2393
+ const solAddr = loaded.yaml.payments[0]?.address ? ` | Solana: ${loaded.yaml.payments[0].address}` : "";
2394
+ const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
2395
+ console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
2396
+ } catch (e) {
2397
+ const hint = /encrypted secrets/i.test(e?.message ?? "") ? " (encrypted)" : "";
2398
+ console.log(` ${agent.name} (${agent.source})${hint}`);
2399
+ }
2400
+ }
2401
+ console.log();
2402
+ })
2403
+ );
2404
+ program.command("wallet [name]").description("Show wallet balance").action(safe(cmdWallet));
2240
2405
  program.parse();
2241
2406
  //# sourceMappingURL=index.js.map
2242
2407
  //# sourceMappingURL=index.js.map