@elisym/cli 0.3.2 → 0.4.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,14 +1,15 @@
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, 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';
10
12
  import pLimit from 'p-limit';
11
- import YAML from 'yaml';
12
13
  import { spawn } from 'node:child_process';
13
14
  import { StringDecoder } from 'node:string_decoder';
14
15
  import { fileURLToPath } from 'node:url';
@@ -22,69 +23,6 @@ var __export = (target, all) => {
22
23
  for (var name in all)
23
24
  __defProp(target, name, { get: all[name], enumerable: true });
24
25
  };
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
26
 
89
27
  // src/commands/init.ts
90
28
  var init_exports = {};
@@ -92,20 +30,6 @@ __export(init_exports, {
92
30
  cmdInit: () => cmdInit,
93
31
  fetchModels: () => fetchModels
94
32
  });
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
33
  async function fetchModels(provider, apiKey) {
110
34
  try {
111
35
  if (provider === "anthropic") {
@@ -138,31 +62,30 @@ async function fetchModels(provider, apiKey) {
138
62
  return FALLBACK_MODELS[provider] ?? ["gpt-4o"];
139
63
  }
140
64
  }
141
- async function cmdInit() {
65
+ function pickTarget(options) {
66
+ return options.local ? "project" : "home";
67
+ }
68
+ async function cmdInit(nameArg, options = {}) {
142
69
  const { default: inquirer } = await import('inquirer');
70
+ const cwd = process.cwd();
143
71
  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)) {
72
+ let template;
73
+ if (options.config) {
74
+ const configPath = resolve(cwd, options.config);
75
+ const raw = readFileSync(configPath, "utf-8");
76
+ template = ElisymYamlSchema.parse(YAML.parse(raw) ?? {});
77
+ console.log(` Loaded template from ${configPath}
78
+ `);
79
+ }
80
+ const agentName = await resolveAgentName(nameArg, inquirer);
81
+ const target = pickTarget(options);
82
+ const sameLocation = target === "home" ? resolveInHome(agentName) : resolveInProject(agentName, cwd);
83
+ if (sameLocation) {
161
84
  const { overwrite } = await inquirer.prompt([
162
85
  {
163
86
  type: "confirm",
164
87
  name: "overwrite",
165
- message: `Agent "${name}" already exists. Overwrite?`,
88
+ message: `Agent "${agentName}" already exists at ${sameLocation}. Overwrite secrets?`,
166
89
  default: false
167
90
  }
168
91
  ]);
@@ -170,7 +93,133 @@ async function cmdInit() {
170
93
  console.log("Aborted.");
171
94
  return;
172
95
  }
96
+ } else if (target === "project" && resolveInHome(agentName)) {
97
+ const { shadow } = await inquirer.prompt([
98
+ {
99
+ type: "confirm",
100
+ name: "shadow",
101
+ message: `A global agent "${agentName}" exists in ~/.elisym/${agentName}/. Create a project-local shadow?`,
102
+ default: true
103
+ }
104
+ ]);
105
+ if (!shadow) {
106
+ console.log("Aborted.");
107
+ return;
108
+ }
109
+ } else if (target === "home" && resolveInProject(agentName, cwd)) {
110
+ const { proceed } = await inquirer.prompt([
111
+ {
112
+ type: "confirm",
113
+ name: "proceed",
114
+ message: `A project-local agent "${agentName}" exists. Create a global agent with the same name?`,
115
+ default: true
116
+ }
117
+ ]);
118
+ if (!proceed) {
119
+ console.log("Aborted.");
120
+ return;
121
+ }
173
122
  }
123
+ let yaml;
124
+ let promptedApiKey;
125
+ if (template) {
126
+ yaml = template;
127
+ } else {
128
+ const result = await promptYaml(inquirer);
129
+ yaml = result.yaml;
130
+ promptedApiKey = result.apiKey;
131
+ }
132
+ let llmApiKey;
133
+ if (yaml.llm) {
134
+ const envKey = yaml.llm.provider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
135
+ if (envKey) {
136
+ llmApiKey = envKey;
137
+ console.log(
138
+ ` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
139
+ );
140
+ } else if (promptedApiKey) {
141
+ llmApiKey = promptedApiKey;
142
+ } else {
143
+ const { apiKey } = await inquirer.prompt([
144
+ {
145
+ type: "password",
146
+ name: "apiKey",
147
+ message: `${yaml.llm.provider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
148
+ mask: "*"
149
+ }
150
+ ]);
151
+ llmApiKey = apiKey || void 0;
152
+ }
153
+ }
154
+ const { passphrase } = await inquirer.prompt([
155
+ {
156
+ type: "password",
157
+ name: "passphrase",
158
+ message: "Passphrase to encrypt secrets (leave empty to skip):",
159
+ mask: "*"
160
+ }
161
+ ]);
162
+ if (passphrase) {
163
+ const { confirmPassphrase } = await inquirer.prompt([
164
+ {
165
+ type: "password",
166
+ name: "confirmPassphrase",
167
+ message: "Confirm passphrase:",
168
+ mask: "*",
169
+ validate: (value) => value === passphrase || "Passphrases do not match"
170
+ }
171
+ ]);
172
+ }
173
+ const nostrSecretBytes = generateSecretKey();
174
+ const nostrPubkey = getPublicKey(nostrSecretBytes);
175
+ const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
176
+ const created = await createAgentDir({ target, name: agentName, cwd });
177
+ await writeYaml(created.dir, yaml);
178
+ await writeSecrets(
179
+ created.dir,
180
+ {
181
+ nostr_secret_key: nostrSecretHex,
182
+ llm_api_key: llmApiKey
183
+ },
184
+ passphrase || void 0
185
+ );
186
+ const npub = nip19.npubEncode(nostrPubkey);
187
+ console.log(`
188
+ Agent "${agentName}" created (${target}).`);
189
+ console.log(` Location: ${created.dir}`);
190
+ console.log(` Nostr: ${npub}`);
191
+ if (yaml.payments[0]?.address) {
192
+ console.log(` Solana: ${yaml.payments[0].address}`);
193
+ }
194
+ if (passphrase) {
195
+ console.log(" Secrets encrypted with your passphrase.");
196
+ }
197
+ console.log();
198
+ }
199
+ async function resolveAgentName(nameArg, inquirer) {
200
+ if (nameArg) {
201
+ validateAgentName(nameArg);
202
+ return nameArg;
203
+ }
204
+ const { inputName } = await inquirer.prompt([
205
+ {
206
+ type: "input",
207
+ name: "inputName",
208
+ message: "Agent name:",
209
+ validate: (value) => {
210
+ try {
211
+ validateAgentName(value);
212
+ return true;
213
+ } catch (e) {
214
+ return e.message;
215
+ }
216
+ }
217
+ }
218
+ ]);
219
+ validateAgentName(inputName);
220
+ return inputName;
221
+ }
222
+ async function promptYaml(inquirer) {
174
223
  const { description } = await inquirer.prompt([
175
224
  {
176
225
  type: "input",
@@ -179,19 +228,27 @@ async function cmdInit() {
179
228
  default: "An elisym AI agent"
180
229
  }
181
230
  ]);
182
- const { pictureInput } = await inquirer.prompt([
231
+ const { displayName } = await inquirer.prompt([
183
232
  {
184
233
  type: "input",
185
- name: "pictureInput",
186
- message: "Avatar image (URL or local path, optional):",
234
+ name: "displayName",
235
+ message: "Display name (optional, for UI):",
187
236
  default: ""
188
237
  }
189
238
  ]);
190
- const { bannerInput } = await inquirer.prompt([
239
+ const { picture } = await inquirer.prompt([
191
240
  {
192
241
  type: "input",
193
- name: "bannerInput",
194
- message: "Banner image (URL or local path, optional):",
242
+ name: "picture",
243
+ message: "Avatar file (relative to agent dir, e.g. ./avatar.png) or URL:",
244
+ default: ""
245
+ }
246
+ ]);
247
+ const { banner } = await inquirer.prompt([
248
+ {
249
+ type: "input",
250
+ name: "banner",
251
+ message: "Banner file (relative to agent dir, e.g. ./header.png) or URL:",
195
252
  default: ""
196
253
  }
197
254
  ]);
@@ -201,23 +258,14 @@ async function cmdInit() {
201
258
  name: "solanaAddress",
202
259
  message: "Solana address for receiving payments (leave empty to skip):",
203
260
  default: "",
204
- validate: (v) => {
205
- if (!v) {
261
+ validate: (value) => {
262
+ if (!value) {
206
263
  return true;
207
264
  }
208
- return isAddress(v) || "Invalid Solana address";
265
+ return isAddress(value) || "Invalid Solana address";
209
266
  }
210
267
  }
211
268
  ]);
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
269
  const { llmProvider } = await inquirer.prompt([
222
270
  {
223
271
  type: "list",
@@ -229,16 +277,21 @@ async function cmdInit() {
229
277
  ]
230
278
  }
231
279
  ]);
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
- ]);
280
+ const envKey = llmProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
281
+ let apiKey = envKey;
282
+ if (!apiKey) {
283
+ const { promptedKey } = await inquirer.prompt([
284
+ {
285
+ type: "password",
286
+ name: "promptedKey",
287
+ message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
288
+ mask: "*"
289
+ }
290
+ ]);
291
+ apiKey = promptedKey || void 0;
292
+ }
240
293
  console.log(" Fetching available models...");
241
- const models = await fetchModels(llmProvider, apiKey);
294
+ const models = await fetchModels(llmProvider, apiKey ?? "");
242
295
  const { model } = await inquirer.prompt([
243
296
  {
244
297
  type: "list",
@@ -255,77 +308,21 @@ async function cmdInit() {
255
308
  default: 4096
256
309
  }
257
310
  ]);
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
- },
311
+ const yaml = ElisymYamlSchema.parse({
312
+ display_name: displayName || void 0,
313
+ description,
314
+ picture: picture || void 0,
315
+ banner: banner || void 0,
302
316
  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
- `);
317
+ payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
318
+ llm: { provider: llmProvider, model, max_tokens: maxTokens },
319
+ security: {}
320
+ });
321
+ return { yaml, apiKey: envKey ? void 0 : apiKey };
324
322
  }
325
323
  var FALLBACK_MODELS;
326
324
  var init_init = __esm({
327
325
  "src/commands/init.ts"() {
328
- init_config();
329
326
  FALLBACK_MODELS = {
330
327
  anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
331
328
  openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
@@ -335,41 +332,32 @@ var init_init = __esm({
335
332
 
336
333
  // src/index.ts
337
334
  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
335
  async function cmdProfile(name) {
356
336
  const { default: inquirer } = await import('inquirer');
337
+ const cwd = process.cwd();
357
338
  if (!name) {
358
- const agents = listAgents();
339
+ const agents = await listAgents(cwd);
359
340
  if (agents.length === 0) {
360
341
  console.error("No agents found. Run `elisym init` first.");
361
342
  process.exit(1);
362
343
  }
363
344
  const { selected } = await inquirer.prompt([
364
- { type: "list", name: "selected", message: "Select agent:", choices: agents }
345
+ {
346
+ type: "list",
347
+ name: "selected",
348
+ message: "Select agent:",
349
+ choices: agents.map((agent) => ({
350
+ name: `${agent.name} (${agent.source})`,
351
+ value: agent.name
352
+ }))
353
+ }
365
354
  ]);
366
355
  name = selected;
367
356
  }
368
357
  const passphrase = process.env.ELISYM_PASSPHRASE;
369
- const config = loadConfig(name, passphrase);
370
- const identity = ElisymIdentity.fromHex(config.identity.secret_key);
358
+ const loaded = await loadAgent(name, cwd, passphrase);
371
359
  console.log(`
372
- Editing agent "${name}"
360
+ Editing agent "${name}" (${loaded.source})
373
361
  `);
374
362
  let done = false;
375
363
  while (!done) {
@@ -379,13 +367,16 @@ async function cmdProfile(name) {
379
367
  name: "section",
380
368
  message: "What to edit?",
381
369
  choices: [
382
- { name: `Profile (name: ${config.identity.name})`, value: "profile" },
383
370
  {
384
- name: `Wallet (${config.payments?.[0]?.address ?? "not configured"})`,
371
+ name: `Profile (description: ${truncate(loaded.yaml.description ?? "")})`,
372
+ value: "profile"
373
+ },
374
+ {
375
+ name: `Wallet (${loaded.yaml.payments[0]?.address ?? "not configured"})`,
385
376
  value: "wallet"
386
377
  },
387
378
  {
388
- name: `LLM (${config.llm?.provider ?? "not configured"} / ${config.llm?.model ?? "-"})`,
379
+ name: `LLM (${loaded.yaml.llm?.provider ?? "not configured"} / ${loaded.yaml.llm?.model ?? "-"})`,
389
380
  value: "llm"
390
381
  },
391
382
  { name: "Done", value: "done" }
@@ -398,70 +389,64 @@ async function cmdProfile(name) {
398
389
  }
399
390
  if (section === "profile") {
400
391
  const answers = await inquirer.prompt([
392
+ {
393
+ type: "input",
394
+ name: "displayName",
395
+ message: "Display name (for UI):",
396
+ default: loaded.yaml.display_name ?? ""
397
+ },
401
398
  {
402
399
  type: "input",
403
400
  name: "description",
404
401
  message: "Description:",
405
- default: config.identity.description ?? ""
402
+ default: loaded.yaml.description ?? ""
406
403
  },
407
404
  {
408
405
  type: "input",
409
406
  name: "picture",
410
- message: "Avatar (URL or local path):",
411
- default: config.identity.picture ?? ""
407
+ message: "Avatar (relative path or URL, empty to clear):",
408
+ default: loaded.yaml.picture ?? ""
412
409
  },
413
410
  {
414
411
  type: "input",
415
412
  name: "banner",
416
- message: "Banner (URL or local path):",
417
- default: config.identity.banner ?? ""
413
+ message: "Banner (relative path or URL, empty to clear):",
414
+ default: loaded.yaml.banner ?? ""
418
415
  }
419
416
  ]);
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);
417
+ const nextYaml = {
418
+ ...loaded.yaml,
419
+ display_name: answers.displayName || void 0,
420
+ description: answers.description ?? "",
421
+ picture: answers.picture || void 0,
422
+ banner: answers.banner || void 0
423
+ };
424
+ await writeYaml(loaded.dir, nextYaml);
425
+ loaded.yaml = nextYaml;
433
426
  console.log(" Profile updated.\n");
434
427
  }
435
428
  if (section === "wallet") {
436
- const currentAddress = config.payments?.[0]?.address ?? "";
437
- const currentNetwork = config.payments?.[0]?.network ?? "devnet";
429
+ const current = loaded.yaml.payments[0];
438
430
  const answers = await inquirer.prompt([
439
431
  {
440
432
  type: "input",
441
433
  name: "address",
442
- message: "Solana address:",
443
- default: currentAddress,
444
- validate: (v) => {
445
- if (!v) {
434
+ message: "Solana address (empty to clear):",
435
+ default: current?.address ?? "",
436
+ validate: (value) => {
437
+ if (!value) {
446
438
  return true;
447
439
  }
448
- return isAddress(v) || "Invalid Solana address";
440
+ return isAddress(value) || "Invalid Solana address";
449
441
  }
450
- },
451
- {
452
- type: "list",
453
- name: "network",
454
- message: "Network:",
455
- choices: ["devnet"],
456
- default: currentNetwork
457
442
  }
458
443
  ]);
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);
444
+ const nextYaml = {
445
+ ...loaded.yaml,
446
+ payments: answers.address ? [{ chain: "solana", network: "devnet", address: answers.address }] : []
447
+ };
448
+ await writeYaml(loaded.dir, nextYaml);
449
+ loaded.yaml = nextYaml;
465
450
  console.log(" Wallet updated.\n");
466
451
  }
467
452
  if (section === "llm") {
@@ -474,33 +459,29 @@ async function cmdProfile(name) {
474
459
  { name: "Anthropic (Claude)", value: "anthropic" },
475
460
  { name: "OpenAI (GPT)", value: "openai" }
476
461
  ],
477
- default: config.llm?.provider ?? "anthropic"
462
+ default: loaded.yaml.llm?.provider ?? "anthropic"
478
463
  }
479
464
  ]);
480
465
  const { apiKey } = await inquirer.prompt([
481
466
  {
482
467
  type: "password",
483
468
  name: "apiKey",
484
- message: `API key (leave empty to keep current):`,
469
+ message: "API key (leave empty to keep current):",
485
470
  mask: "*"
486
471
  }
487
472
  ]);
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
- }
473
+ const currentKeyPlain = loaded.secrets.llm_api_key && !isEncrypted(loaded.secrets.llm_api_key) ? loaded.secrets.llm_api_key : "";
474
+ const probeKey = apiKey || currentKeyPlain;
494
475
  console.log(" Fetching available models...");
495
476
  const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
496
- const models = await fetchModels2(llmProvider, plainKey);
477
+ const models = await fetchModels2(llmProvider, probeKey);
497
478
  const { model } = await inquirer.prompt([
498
479
  {
499
480
  type: "list",
500
481
  name: "model",
501
482
  message: "Model:",
502
483
  choices: models,
503
- default: config.llm?.model
484
+ default: loaded.yaml.llm?.model
504
485
  }
505
486
  ]);
506
487
  const { maxTokens } = await inquirer.prompt([
@@ -508,26 +489,29 @@ async function cmdProfile(name) {
508
489
  type: "number",
509
490
  name: "maxTokens",
510
491
  message: "Max tokens:",
511
- default: config.llm?.max_tokens ?? 4096
492
+ default: loaded.yaml.llm?.max_tokens ?? 4096
512
493
  }
513
494
  ]);
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);
495
+ const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
496
+ const nextYaml = { ...loaded.yaml, llm: nextLlm };
497
+ await writeYaml(loaded.dir, nextYaml);
498
+ loaded.yaml = nextYaml;
499
+ if (apiKey) {
500
+ await writeSecrets(loaded.dir, { ...loaded.secrets, llm_api_key: apiKey }, passphrase);
501
+ loaded.secrets = { ...loaded.secrets, llm_api_key: apiKey };
502
+ }
522
503
  console.log(" LLM updated.\n");
523
504
  }
524
505
  }
525
506
  console.log(` Agent "${name}" saved.
526
507
  `);
527
508
  }
528
-
529
- // src/commands/start.ts
530
- init_config();
509
+ function truncate(value, max = 40) {
510
+ if (value.length <= max) {
511
+ return value;
512
+ }
513
+ return value.slice(0, max - 1) + "\u2026";
514
+ }
531
515
  var HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
532
516
  var MAX_CONCURRENT_JOBS = 10;
533
517
  var RECOVERY_MAX_RETRIES = 5;
@@ -552,8 +536,13 @@ var VALID_TRANSITIONS = {
552
536
  var JobLedger = class {
553
537
  entries = /* @__PURE__ */ new Map();
554
538
  path;
555
- constructor(agentName) {
556
- this.path = join(homedir(), ".elisym", "agents", agentName, "jobs.json");
539
+ /**
540
+ * @param ledgerPath absolute path to the ledger file
541
+ * (typically `<agentDir>/.jobs.json`). Caller is responsible for
542
+ * resolving the agent directory via `@elisym/sdk/agent-store`.
543
+ */
544
+ constructor(ledgerPath) {
545
+ this.path = ledgerPath;
557
546
  this.load();
558
547
  }
559
548
  load() {
@@ -677,7 +666,7 @@ function sleepWithSignal(ms, signal) {
677
666
  if (!signal) {
678
667
  return new Promise((r) => setTimeout(r, ms));
679
668
  }
680
- return new Promise((resolve, reject) => {
669
+ return new Promise((resolve3, reject) => {
681
670
  const cleanup = () => {
682
671
  clearTimeout(timer);
683
672
  signal.removeEventListener("abort", onAbort);
@@ -688,7 +677,7 @@ function sleepWithSignal(ms, signal) {
688
677
  };
689
678
  const timer = setTimeout(() => {
690
679
  cleanup();
691
- resolve();
680
+ resolve3();
692
681
  }, ms);
693
682
  signal.addEventListener("abort", onAbort, { once: true });
694
683
  });
@@ -1055,8 +1044,8 @@ var AgentRuntime = class {
1055
1044
  });
1056
1045
  });
1057
1046
  log("Agent runtime started. Listening for jobs...");
1058
- await new Promise((resolve) => {
1059
- this.abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1047
+ await new Promise((resolve3) => {
1048
+ this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
1060
1049
  process.on("SIGINT", () => {
1061
1050
  log("Shutting down...");
1062
1051
  this.stop();
@@ -1553,7 +1542,7 @@ var ScriptSkill = class {
1553
1542
  throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
1554
1543
  }
1555
1544
  runTool(toolDef, call, signal) {
1556
- return new Promise((resolve, _reject) => {
1545
+ return new Promise((resolve3, _reject) => {
1557
1546
  const args = [...toolDef.command];
1558
1547
  const cmd = args.shift();
1559
1548
  const params = toolDef.parameters ?? [];
@@ -1596,13 +1585,13 @@ var ScriptSkill = class {
1596
1585
  });
1597
1586
  child.on("close", (code) => {
1598
1587
  if (code === 0) {
1599
- resolve(stdout.trim());
1588
+ resolve3(stdout.trim());
1600
1589
  } else {
1601
- resolve(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1590
+ resolve3(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1602
1591
  }
1603
1592
  });
1604
1593
  child.on("error", (err) => {
1605
- resolve(`Error: ${err.message}`);
1594
+ resolve3(`Error: ${err.message}`);
1606
1595
  });
1607
1596
  });
1608
1597
  }
@@ -1868,37 +1857,25 @@ function startWatchdog(deps) {
1868
1857
  }
1869
1858
 
1870
1859
  // 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;
1860
+ async function cmdStart(nameArg) {
1861
+ const cwd = process.cwd();
1862
+ const agentName = await resolveStartAgentName(nameArg, cwd);
1863
+ if (!agentName) {
1864
+ return;
1889
1865
  }
1890
- const config = await loadConfigWithPrompt(name);
1866
+ const loaded = await loadAgentWithPrompt(agentName, cwd);
1891
1867
  console.log(`
1892
- Starting agent ${name}...
1868
+ Starting agent ${agentName} (${loaded.source})...
1893
1869
  `);
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
- }
1870
+ if (loaded.shadowsGlobal) {
1871
+ console.log(
1872
+ ` ! Using project-local ${agentName} (shadows global agent in ~/.elisym/${agentName}/)
1873
+ `
1874
+ );
1900
1875
  }
1901
- const walletNetwork = config.payments?.find((p) => p.chain === "solana")?.network ?? "devnet";
1876
+ const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
1877
+ const solanaAddress = solPayment?.address;
1878
+ const walletNetwork = solPayment?.network ?? "devnet";
1902
1879
  if (solanaAddress) {
1903
1880
  try {
1904
1881
  const rpcUrl = getRpcUrl(walletNetwork);
@@ -1922,17 +1899,26 @@ async function cmdStart(name) {
1922
1899
  `);
1923
1900
  }
1924
1901
  }
1925
- if (!config.llm) {
1902
+ if (!loaded.yaml.llm) {
1926
1903
  console.error(" ! No LLM configured. Run `elisym init` to set up LLM.\n");
1927
1904
  process.exit(1);
1928
1905
  }
1929
- const skillsDir = join(process.cwd(), "skills");
1906
+ if (!loaded.secrets.llm_api_key) {
1907
+ console.error(
1908
+ ` ! No LLM API key. Set ${loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} env var or update the agent's secrets.
1909
+ `
1910
+ );
1911
+ process.exit(1);
1912
+ }
1913
+ const paths = agentPaths(loaded.dir);
1914
+ const skillsDir = paths.skills;
1930
1915
  const allSkills = loadSkillsFromDir(skillsDir);
1931
1916
  if (allSkills.length === 0) {
1932
1917
  console.error(` ! No skills found in ${skillsDir}
1933
1918
  `);
1934
1919
  console.error(" Create a skill directory with a SKILL.md to get started.");
1935
- console.error(" Example: ./skills/my-skill/SKILL.md\n");
1920
+ console.error(` Example: ${skillsDir}/my-skill/SKILL.md
1921
+ `);
1936
1922
  process.exit(1);
1937
1923
  }
1938
1924
  const registry = new SkillRegistry();
@@ -1948,50 +1934,69 @@ async function cmdStart(name) {
1948
1934
  process.exit(1);
1949
1935
  }
1950
1936
  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,
1937
+ provider: loaded.yaml.llm.provider,
1938
+ apiKey: loaded.secrets.llm_api_key,
1939
+ model: loaded.yaml.llm.model,
1940
+ maxTokens: loaded.yaml.llm.max_tokens,
1955
1941
  logUsage: true
1956
1942
  });
1957
1943
  const skillCtx = {
1958
1944
  llm,
1959
- agentName: config.identity.name,
1960
- agentDescription: config.identity.description ?? ""
1945
+ agentName,
1946
+ agentDescription: loaded.yaml.description ?? ""
1961
1947
  };
1962
1948
  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];
1949
+ const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
1950
+ const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
1965
1951
  const client = new ElisymClient({ relays });
1966
1952
  const media = new MediaService();
1953
+ const mediaCache = await readMediaCache(loaded.dir);
1954
+ let mediaCacheDirty = false;
1955
+ const pictureUrl = await resolveMediaField(
1956
+ loaded.yaml.picture,
1957
+ loaded.dir,
1958
+ mediaCache,
1959
+ media,
1960
+ identity,
1961
+ (updated) => mediaCacheDirty = mediaCacheDirty || updated
1962
+ );
1963
+ const bannerUrl = await resolveMediaField(
1964
+ loaded.yaml.banner,
1965
+ loaded.dir,
1966
+ mediaCache,
1967
+ media,
1968
+ identity,
1969
+ (updated) => mediaCacheDirty = mediaCacheDirty || updated
1970
+ );
1967
1971
  for (const skill of allSkills) {
1968
1972
  if (skill.image || !skill.imageFile) {
1969
1973
  continue;
1970
1974
  }
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}`);
1975
+ const skillRoot = join(skillsDir, skill.name);
1976
+ const cacheKey = `./skills/${skill.name}/${skill.imageFile}`;
1977
+ const absPath = join(skillRoot, skill.imageFile);
1978
+ const url = await uploadOrReuse(
1979
+ cacheKey,
1980
+ absPath,
1981
+ mediaCache,
1982
+ media,
1983
+ identity,
1984
+ () => mediaCacheDirty = true
1985
+ );
1986
+ if (url) {
1978
1987
  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
1988
  }
1987
1989
  }
1990
+ if (mediaCacheDirty) {
1991
+ await writeMediaCache(loaded.dir, mediaCache);
1992
+ }
1988
1993
  try {
1989
1994
  await client.discovery.publishProfile(
1990
1995
  identity,
1991
- config.identity.name,
1992
- config.identity.description ?? "",
1993
- config.identity.picture,
1994
- config.identity.banner
1996
+ agentName,
1997
+ loaded.yaml.description ?? "",
1998
+ pictureUrl,
1999
+ bannerUrl
1995
2000
  );
1996
2001
  } catch (e) {
1997
2002
  console.warn(` ! Failed to publish profile: ${e.message}`);
@@ -2057,7 +2062,7 @@ image: ${url}`);
2057
2062
  }, HEARTBEAT_INTERVAL_MS);
2058
2063
  }
2059
2064
  const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
2060
- const ledger = new JobLedger(name);
2065
+ const ledger = new JobLedger(paths.jobs);
2061
2066
  const runtimeConfig = {
2062
2067
  paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
2063
2068
  maxConcurrentJobs: MAX_CONCURRENT_JOBS,
@@ -2096,15 +2101,88 @@ image: ${url}`);
2096
2101
  console.log(" * Running. Press Ctrl+C to stop.\n");
2097
2102
  await runtime.run();
2098
2103
  }
2104
+ async function resolveMediaField(value, agentDir, cache, media, identity, onCacheUpdate) {
2105
+ if (!value) {
2106
+ return void 0;
2107
+ }
2108
+ if (isRemoteUrl(value)) {
2109
+ return value;
2110
+ }
2111
+ const absPath = resolveInsideAgentDir(value, agentDir);
2112
+ if (!absPath) {
2113
+ console.warn(` ! Skipping media field "${value}": path must stay inside the agent directory.`);
2114
+ return void 0;
2115
+ }
2116
+ return uploadOrReuse(value, absPath, cache, media, identity, () => onCacheUpdate(true));
2117
+ }
2118
+ function resolveInsideAgentDir(value, agentDir) {
2119
+ const agentRoot = resolve(agentDir);
2120
+ const candidate = resolve(agentRoot, value);
2121
+ const rel = relative(agentRoot, candidate);
2122
+ if (rel === "" || rel.startsWith("..") || rel.includes(`..${sep}`)) {
2123
+ return null;
2124
+ }
2125
+ return candidate;
2126
+ }
2127
+ async function uploadOrReuse(cacheKey, absPath, cache, media, identity, onCacheUpdate) {
2128
+ try {
2129
+ const cached = await lookupCachedUrl(cache, cacheKey, absPath);
2130
+ if (cached) {
2131
+ return cached;
2132
+ }
2133
+ console.log(` Uploading ${basename(absPath)}...`);
2134
+ const data = readFileSync(absPath);
2135
+ const sha256 = createHash("sha256").update(data).digest("hex");
2136
+ const blob = new Blob([data]);
2137
+ const url = await media.upload(identity, blob, basename(absPath));
2138
+ cache[cacheKey] = newCacheEntry(url, sha256);
2139
+ onCacheUpdate();
2140
+ console.log(` Uploaded: ${url}`);
2141
+ return url;
2142
+ } catch (e) {
2143
+ console.warn(` ! Failed to upload ${basename(absPath)}: ${e.message}`);
2144
+ return void 0;
2145
+ }
2146
+ }
2147
+ function isRemoteUrl(value) {
2148
+ return /^https?:\/\//i.test(value);
2149
+ }
2099
2150
  var MAX_PASSPHRASE_ATTEMPTS = 3;
2100
- async function loadConfigWithPrompt(name) {
2151
+ async function resolveStartAgentName(nameArg, cwd) {
2152
+ if (nameArg) {
2153
+ return nameArg;
2154
+ }
2155
+ const agents = await listAgents(cwd);
2156
+ if (agents.length === 0) {
2157
+ console.log("No agents configured. Run `elisym init` first.");
2158
+ process.exit(1);
2159
+ }
2160
+ const { default: inquirer } = await import('inquirer');
2161
+ const choices = [
2162
+ ...agents.map((agent) => ({
2163
+ name: `${agent.name} (${agent.source}${agent.shadowsGlobal ? " - shadows global" : ""})`,
2164
+ value: agent.name
2165
+ })),
2166
+ { name: "+ Create new agent", value: "__new__" }
2167
+ ];
2168
+ const { selected } = await inquirer.prompt([
2169
+ { type: "list", name: "selected", message: "Select agent to start", choices }
2170
+ ]);
2171
+ if (selected === "__new__") {
2172
+ const { cmdInit: cmdInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2173
+ await cmdInit2();
2174
+ return void 0;
2175
+ }
2176
+ return selected;
2177
+ }
2178
+ async function loadAgentWithPrompt(name, cwd) {
2101
2179
  const envPassphrase = process.env.ELISYM_PASSPHRASE;
2102
2180
  try {
2103
- return loadConfig(name, envPassphrase);
2181
+ return await loadAgent(name, cwd, envPassphrase);
2104
2182
  } catch (e) {
2105
- const isEncryptedConfigError = /encrypted but no passphrase/i.test(e?.message ?? "");
2183
+ const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2106
2184
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2107
- if (!isEncryptedConfigError && !isWrongPassphrase) {
2185
+ if (!isEncrypted3 && !isWrongPassphrase) {
2108
2186
  throw e;
2109
2187
  }
2110
2188
  if (!process.stdin.isTTY) {
@@ -2122,7 +2200,7 @@ async function loadConfigWithPrompt(name) {
2122
2200
  }
2123
2201
  ]);
2124
2202
  try {
2125
- return loadConfig(name, passphrase);
2203
+ return await loadAgent(name, cwd, passphrase);
2126
2204
  } catch (e) {
2127
2205
  if (!/Decryption failed/i.test(e?.message ?? "")) {
2128
2206
  throw e;
@@ -2137,12 +2215,10 @@ async function loadConfigWithPrompt(name) {
2137
2215
  }
2138
2216
  throw new Error("Unreachable");
2139
2217
  }
2140
-
2141
- // src/commands/wallet.ts
2142
- init_config();
2143
2218
  async function cmdWallet(name) {
2219
+ const cwd = process.cwd();
2144
2220
  if (!name) {
2145
- const agents = listAgents();
2221
+ const agents = await listAgents(cwd);
2146
2222
  if (agents.length === 0) {
2147
2223
  console.error("No agents found.");
2148
2224
  process.exit(1);
@@ -2153,33 +2229,32 @@ async function cmdWallet(name) {
2153
2229
  type: "list",
2154
2230
  name: "selected",
2155
2231
  message: "Select agent:",
2156
- choices: agents
2232
+ choices: agents.map((agent) => ({
2233
+ name: `${agent.name} (${agent.source})`,
2234
+ value: agent.name
2235
+ }))
2157
2236
  }
2158
2237
  ]);
2159
2238
  name = selected;
2160
2239
  }
2161
2240
  const passphrase = process.env.ELISYM_PASSPHRASE;
2162
- const config = loadConfig(name, passphrase);
2163
- const solPayment = config.payments?.find((p) => p.chain === "solana");
2241
+ const loaded = await loadAgent(name, cwd, passphrase);
2242
+ const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
2164
2243
  if (!solPayment?.address) {
2165
2244
  console.error("Solana address not configured for this agent.");
2166
2245
  process.exit(1);
2167
2246
  }
2168
- const network = solPayment.network ?? "devnet";
2169
- const rpcUrl = getRpcUrl();
2247
+ const rpcUrl = getRpcUrl(solPayment.network);
2170
2248
  const rpc = createSolanaRpc(rpcUrl);
2171
2249
  const walletAddress = address(solPayment.address);
2172
2250
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2173
2251
  console.log(`
2174
2252
  Agent: ${name}`);
2175
- console.log(` Network: ${network}`);
2253
+ console.log(` Network: ${solPayment.network}`);
2176
2254
  console.log(` Address: ${solPayment.address}`);
2177
2255
  console.log(` Balance: ${formatSol(Number(balance))} (${balance} lamports)
2178
2256
  `);
2179
2257
  }
2180
-
2181
- // src/index.ts
2182
- init_config();
2183
2258
  function readPackageVersion() {
2184
2259
  try {
2185
2260
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2193,50 +2268,50 @@ var PACKAGE_VERSION = readPackageVersion();
2193
2268
 
2194
2269
  // src/index.ts
2195
2270
  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) {
2271
+ function safe(fn) {
2272
+ return async (...args) => {
2208
2273
  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)`);
2274
+ await fn(...args);
2275
+ } catch (e) {
2276
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
2277
+ process.exit(1);
2220
2278
  }
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
2279
+ };
2280
+ }
2281
+ var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
2282
+ 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(
2283
+ safe(async (name, options) => {
2284
+ await cmdInit(name, options);
2285
+ })
2286
+ );
2287
+ program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(safe(cmdProfile));
2288
+ program.command("start [name]").description("Start agent in provider mode").action(safe(cmdStart));
2289
+ program.command("list").description("List all agents (project-local and home-global)").action(
2290
+ safe(async () => {
2291
+ const cwd = process.cwd();
2292
+ const agents = await listAgents(cwd);
2293
+ if (agents.length === 0) {
2294
+ console.log("No agents found. Run `elisym init` to create one.");
2295
+ return;
2233
2296
  }
2234
- ]);
2235
- if (confirm) {
2236
- deleteAgent(name);
2237
- console.log(`Agent "${name}" deleted.`);
2238
- }
2239
- });
2297
+ console.log("\nAgents:");
2298
+ for (const agent of agents) {
2299
+ try {
2300
+ const loaded = await loadAgent(agent.name, cwd);
2301
+ const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
2302
+ const npub = nip19.npubEncode(identity.publicKey);
2303
+ const solAddr = loaded.yaml.payments[0]?.address ? ` | Solana: ${loaded.yaml.payments[0].address}` : "";
2304
+ const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
2305
+ console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
2306
+ } catch (e) {
2307
+ const hint = /encrypted secrets/i.test(e?.message ?? "") ? " (encrypted)" : "";
2308
+ console.log(` ${agent.name} (${agent.source})${hint}`);
2309
+ }
2310
+ }
2311
+ console.log();
2312
+ })
2313
+ );
2314
+ program.command("wallet [name]").description("Show wallet balance").action(safe(cmdWallet));
2240
2315
  program.parse();
2241
2316
  //# sourceMappingURL=index.js.map
2242
2317
  //# sourceMappingURL=index.js.map