@elisym/cli 0.3.1 → 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([
232
+ {
233
+ type: "input",
234
+ name: "displayName",
235
+ message: "Display name (optional, for UI):",
236
+ default: ""
237
+ }
238
+ ]);
239
+ const { picture } = await inquirer.prompt([
183
240
  {
184
241
  type: "input",
185
- name: "pictureInput",
186
- message: "Avatar image (URL or local path, optional):",
242
+ name: "picture",
243
+ message: "Avatar file (relative to agent dir, e.g. ./avatar.png) or URL:",
187
244
  default: ""
188
245
  }
189
246
  ]);
190
- const { bannerInput } = await inquirer.prompt([
247
+ const { banner } = await inquirer.prompt([
191
248
  {
192
249
  type: "input",
193
- name: "bannerInput",
194
- message: "Banner image (URL or local path, optional):",
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,27 +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: [
218
- { name: "devnet", value: "devnet" },
219
- { name: "testnet", value: "testnet" },
220
- { name: "mainnet", value: "mainnet" }
221
- ],
222
- default: "devnet"
223
- }
224
- ]);
225
269
  const { llmProvider } = await inquirer.prompt([
226
270
  {
227
271
  type: "list",
@@ -233,16 +277,21 @@ async function cmdInit() {
233
277
  ]
234
278
  }
235
279
  ]);
236
- const { apiKey } = await inquirer.prompt([
237
- {
238
- type: "password",
239
- name: "apiKey",
240
- message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
241
- mask: "*"
242
- }
243
- ]);
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
+ }
244
293
  console.log(" Fetching available models...");
245
- const models = await fetchModels(llmProvider, apiKey);
294
+ const models = await fetchModels(llmProvider, apiKey ?? "");
246
295
  const { model } = await inquirer.prompt([
247
296
  {
248
297
  type: "list",
@@ -259,77 +308,21 @@ async function cmdInit() {
259
308
  default: 4096
260
309
  }
261
310
  ]);
262
- const { passphrase } = await inquirer.prompt([
263
- {
264
- type: "password",
265
- name: "passphrase",
266
- message: "Passphrase to encrypt secrets (leave empty to skip):",
267
- mask: "*"
268
- }
269
- ]);
270
- if (passphrase) {
271
- const { confirmPassphrase } = await inquirer.prompt([
272
- {
273
- type: "password",
274
- name: "confirmPassphrase",
275
- message: "Confirm passphrase:",
276
- mask: "*",
277
- validate: (v) => v === passphrase ? true : "Passphrases do not match"
278
- }
279
- ]);
280
- }
281
- const nostrSecretKey = generateSecretKey();
282
- const nostrPubkey = getPublicKey(nostrSecretKey);
283
- const nostrSecretHex = Buffer.from(nostrSecretKey).toString("hex");
284
- const identity = ElisymIdentity.fromHex(nostrSecretHex);
285
- const media = new MediaService();
286
- let picture = "";
287
- let banner = "";
288
- if (pictureInput || bannerInput) {
289
- console.log();
290
- if (pictureInput) {
291
- picture = await resolveImage(pictureInput, identity, media);
292
- }
293
- if (bannerInput) {
294
- banner = await resolveImage(bannerInput, identity, media);
295
- }
296
- }
297
- const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
298
- const config = {
299
- identity: {
300
- secret_key: protect(nostrSecretHex),
301
- name,
302
- description,
303
- picture: picture || void 0,
304
- banner: banner || void 0
305
- },
311
+ const yaml = ElisymYamlSchema.parse({
312
+ display_name: displayName || void 0,
313
+ description,
314
+ picture: picture || void 0,
315
+ banner: banner || void 0,
306
316
  relays: [...RELAYS],
307
- payments: solanaAddress ? [{ chain: "solana", network, address: solanaAddress }] : void 0,
308
- llm: {
309
- provider: llmProvider,
310
- api_key: protect(apiKey),
311
- model,
312
- max_tokens: maxTokens
313
- }
314
- };
315
- saveConfig(config);
316
- const npub = nip19.npubEncode(nostrPubkey);
317
- console.log(`
318
- Agent "${name}" created.`);
319
- console.log(` Nostr: ${npub}`);
320
- if (solanaAddress) {
321
- console.log(` Solana: ${solanaAddress}`);
322
- }
323
- if (passphrase) {
324
- console.log(" Secrets encrypted with your passphrase.");
325
- }
326
- console.log(` Config: ~/.elisym/agents/${name}/config.json
327
- `);
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 };
328
322
  }
329
323
  var FALLBACK_MODELS;
330
324
  var init_init = __esm({
331
325
  "src/commands/init.ts"() {
332
- init_config();
333
326
  FALLBACK_MODELS = {
334
327
  anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
335
328
  openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
@@ -339,41 +332,32 @@ var init_init = __esm({
339
332
 
340
333
  // src/index.ts
341
334
  init_init();
342
-
343
- // src/commands/profile.ts
344
- init_config();
345
- async function resolveImage2(input, identity, media) {
346
- if (!input) {
347
- return "";
348
- }
349
- if (existsSync(input)) {
350
- console.log(` Uploading ${basename(input)}...`);
351
- const data = readFileSync(input);
352
- const blob = new Blob([data]);
353
- const url = await media.upload(identity, blob, basename(input));
354
- console.log(` Uploaded: ${url}`);
355
- return url;
356
- }
357
- return input;
358
- }
359
335
  async function cmdProfile(name) {
360
336
  const { default: inquirer } = await import('inquirer');
337
+ const cwd = process.cwd();
361
338
  if (!name) {
362
- const agents = listAgents();
339
+ const agents = await listAgents(cwd);
363
340
  if (agents.length === 0) {
364
341
  console.error("No agents found. Run `elisym init` first.");
365
342
  process.exit(1);
366
343
  }
367
344
  const { selected } = await inquirer.prompt([
368
- { 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
+ }
369
354
  ]);
370
355
  name = selected;
371
356
  }
372
357
  const passphrase = process.env.ELISYM_PASSPHRASE;
373
- const config = loadConfig(name, passphrase);
374
- const identity = ElisymIdentity.fromHex(config.identity.secret_key);
358
+ const loaded = await loadAgent(name, cwd, passphrase);
375
359
  console.log(`
376
- Editing agent "${name}"
360
+ Editing agent "${name}" (${loaded.source})
377
361
  `);
378
362
  let done = false;
379
363
  while (!done) {
@@ -383,13 +367,16 @@ async function cmdProfile(name) {
383
367
  name: "section",
384
368
  message: "What to edit?",
385
369
  choices: [
386
- { name: `Profile (name: ${config.identity.name})`, value: "profile" },
387
370
  {
388
- 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"})`,
389
376
  value: "wallet"
390
377
  },
391
378
  {
392
- name: `LLM (${config.llm?.provider ?? "not configured"} / ${config.llm?.model ?? "-"})`,
379
+ name: `LLM (${loaded.yaml.llm?.provider ?? "not configured"} / ${loaded.yaml.llm?.model ?? "-"})`,
393
380
  value: "llm"
394
381
  },
395
382
  { name: "Done", value: "done" }
@@ -402,70 +389,64 @@ async function cmdProfile(name) {
402
389
  }
403
390
  if (section === "profile") {
404
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
+ },
405
398
  {
406
399
  type: "input",
407
400
  name: "description",
408
401
  message: "Description:",
409
- default: config.identity.description ?? ""
402
+ default: loaded.yaml.description ?? ""
410
403
  },
411
404
  {
412
405
  type: "input",
413
406
  name: "picture",
414
- message: "Avatar (URL or local path):",
415
- default: config.identity.picture ?? ""
407
+ message: "Avatar (relative path or URL, empty to clear):",
408
+ default: loaded.yaml.picture ?? ""
416
409
  },
417
410
  {
418
411
  type: "input",
419
412
  name: "banner",
420
- message: "Banner (URL or local path):",
421
- default: config.identity.banner ?? ""
413
+ message: "Banner (relative path or URL, empty to clear):",
414
+ default: loaded.yaml.banner ?? ""
422
415
  }
423
416
  ]);
424
- config.identity.description = answers.description || void 0;
425
- const media = new MediaService();
426
- if (answers.picture) {
427
- config.identity.picture = await resolveImage2(answers.picture, identity, media);
428
- } else {
429
- config.identity.picture = void 0;
430
- }
431
- if (answers.banner) {
432
- config.identity.banner = await resolveImage2(answers.banner, identity, media);
433
- } else {
434
- config.identity.banner = void 0;
435
- }
436
- 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;
437
426
  console.log(" Profile updated.\n");
438
427
  }
439
428
  if (section === "wallet") {
440
- const currentAddress = config.payments?.[0]?.address ?? "";
441
- const currentNetwork = config.payments?.[0]?.network ?? "devnet";
429
+ const current = loaded.yaml.payments[0];
442
430
  const answers = await inquirer.prompt([
443
431
  {
444
432
  type: "input",
445
433
  name: "address",
446
- message: "Solana address:",
447
- default: currentAddress,
448
- validate: (v) => {
449
- if (!v) {
434
+ message: "Solana address (empty to clear):",
435
+ default: current?.address ?? "",
436
+ validate: (value) => {
437
+ if (!value) {
450
438
  return true;
451
439
  }
452
- return isAddress(v) || "Invalid Solana address";
440
+ return isAddress(value) || "Invalid Solana address";
453
441
  }
454
- },
455
- {
456
- type: "list",
457
- name: "network",
458
- message: "Network:",
459
- choices: ["devnet", "testnet", "mainnet"],
460
- default: currentNetwork
461
442
  }
462
443
  ]);
463
- if (answers.address) {
464
- config.payments = [{ chain: "solana", network: answers.network, address: answers.address }];
465
- } else {
466
- config.payments = void 0;
467
- }
468
- 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;
469
450
  console.log(" Wallet updated.\n");
470
451
  }
471
452
  if (section === "llm") {
@@ -478,33 +459,29 @@ async function cmdProfile(name) {
478
459
  { name: "Anthropic (Claude)", value: "anthropic" },
479
460
  { name: "OpenAI (GPT)", value: "openai" }
480
461
  ],
481
- default: config.llm?.provider ?? "anthropic"
462
+ default: loaded.yaml.llm?.provider ?? "anthropic"
482
463
  }
483
464
  ]);
484
465
  const { apiKey } = await inquirer.prompt([
485
466
  {
486
467
  type: "password",
487
468
  name: "apiKey",
488
- message: `API key (leave empty to keep current):`,
469
+ message: "API key (leave empty to keep current):",
489
470
  mask: "*"
490
471
  }
491
472
  ]);
492
- const effectiveKey = apiKey || config.llm?.api_key || "";
493
- let plainKey = effectiveKey;
494
- if (isEncrypted(plainKey) && passphrase) {
495
- const { decryptSecret } = await import('@elisym/sdk/node');
496
- plainKey = decryptSecret(plainKey, passphrase);
497
- }
473
+ const currentKeyPlain = loaded.secrets.llm_api_key && !isEncrypted(loaded.secrets.llm_api_key) ? loaded.secrets.llm_api_key : "";
474
+ const probeKey = apiKey || currentKeyPlain;
498
475
  console.log(" Fetching available models...");
499
476
  const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
500
- const models = await fetchModels2(llmProvider, plainKey);
477
+ const models = await fetchModels2(llmProvider, probeKey);
501
478
  const { model } = await inquirer.prompt([
502
479
  {
503
480
  type: "list",
504
481
  name: "model",
505
482
  message: "Model:",
506
483
  choices: models,
507
- default: config.llm?.model
484
+ default: loaded.yaml.llm?.model
508
485
  }
509
486
  ]);
510
487
  const { maxTokens } = await inquirer.prompt([
@@ -512,26 +489,29 @@ async function cmdProfile(name) {
512
489
  type: "number",
513
490
  name: "maxTokens",
514
491
  message: "Max tokens:",
515
- default: config.llm?.max_tokens ?? 4096
492
+ default: loaded.yaml.llm?.max_tokens ?? 4096
516
493
  }
517
494
  ]);
518
- const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
519
- config.llm = {
520
- provider: llmProvider,
521
- api_key: apiKey ? protect(apiKey) : effectiveKey,
522
- model,
523
- max_tokens: maxTokens
524
- };
525
- 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
+ }
526
503
  console.log(" LLM updated.\n");
527
504
  }
528
505
  }
529
506
  console.log(` Agent "${name}" saved.
530
507
  `);
531
508
  }
532
-
533
- // src/commands/start.ts
534
- 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
+ }
535
515
  var HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
536
516
  var MAX_CONCURRENT_JOBS = 10;
537
517
  var RECOVERY_MAX_RETRIES = 5;
@@ -540,19 +520,12 @@ var WATCHDOG_PROBE_INTERVAL_MS = 5 * 60 * 1e3;
540
520
  var WATCHDOG_PROBE_TIMEOUT_MS = 1e4;
541
521
  var WATCHDOG_SELF_PING_INTERVAL_MS = 10 * 60 * 1e3;
542
522
  var WATCHDOG_SELF_PING_TIMEOUT_MS = 15e3;
543
- function getRpcUrl(network) {
523
+ function getRpcUrl(_network) {
544
524
  const envUrl = process.env.SOLANA_RPC_URL;
545
525
  if (envUrl) {
546
526
  return envUrl;
547
527
  }
548
- switch (network) {
549
- case "mainnet":
550
- return "https://api.mainnet-beta.solana.com";
551
- case "testnet":
552
- return "https://api.testnet.solana.com";
553
- default:
554
- return "https://api.devnet.solana.com";
555
- }
528
+ return "https://api.devnet.solana.com";
556
529
  }
557
530
  var VALID_TRANSITIONS = {
558
531
  paid: ["executed", "failed"],
@@ -563,8 +536,13 @@ var VALID_TRANSITIONS = {
563
536
  var JobLedger = class {
564
537
  entries = /* @__PURE__ */ new Map();
565
538
  path;
566
- constructor(agentName) {
567
- 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;
568
546
  this.load();
569
547
  }
570
548
  load() {
@@ -688,7 +666,7 @@ function sleepWithSignal(ms, signal) {
688
666
  if (!signal) {
689
667
  return new Promise((r) => setTimeout(r, ms));
690
668
  }
691
- return new Promise((resolve, reject) => {
669
+ return new Promise((resolve3, reject) => {
692
670
  const cleanup = () => {
693
671
  clearTimeout(timer);
694
672
  signal.removeEventListener("abort", onAbort);
@@ -699,7 +677,7 @@ function sleepWithSignal(ms, signal) {
699
677
  };
700
678
  const timer = setTimeout(() => {
701
679
  cleanup();
702
- resolve();
680
+ resolve3();
703
681
  }, ms);
704
682
  signal.addEventListener("abort", onAbort, { once: true });
705
683
  });
@@ -1000,8 +978,12 @@ var AgentRuntime = class {
1000
978
  globalJobs = [];
1001
979
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
1002
980
  async fetchProtocolConfig() {
1003
- const cluster = this.config.network === "mainnet" ? "mainnet" : "devnet";
1004
- const programId = getProtocolProgramId(cluster);
981
+ if (this.config.network !== "devnet") {
982
+ throw new Error(
983
+ `Network "${this.config.network}" is not supported. Only "devnet" is available until the on-chain protocol program is deployed on mainnet.`
984
+ );
985
+ }
986
+ const programId = getProtocolProgramId("devnet");
1005
987
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
1006
988
  const config = await getProtocolConfig(rpc, programId, { forceRefresh: true });
1007
989
  return { feeBps: config.feeBps, treasury: config.treasury };
@@ -1062,8 +1044,8 @@ var AgentRuntime = class {
1062
1044
  });
1063
1045
  });
1064
1046
  log("Agent runtime started. Listening for jobs...");
1065
- await new Promise((resolve) => {
1066
- this.abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1047
+ await new Promise((resolve3) => {
1048
+ this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
1067
1049
  process.on("SIGINT", () => {
1068
1050
  log("Shutting down...");
1069
1051
  this.stop();
@@ -1560,7 +1542,7 @@ var ScriptSkill = class {
1560
1542
  throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
1561
1543
  }
1562
1544
  runTool(toolDef, call, signal) {
1563
- return new Promise((resolve, _reject) => {
1545
+ return new Promise((resolve3, _reject) => {
1564
1546
  const args = [...toolDef.command];
1565
1547
  const cmd = args.shift();
1566
1548
  const params = toolDef.parameters ?? [];
@@ -1603,13 +1585,13 @@ var ScriptSkill = class {
1603
1585
  });
1604
1586
  child.on("close", (code) => {
1605
1587
  if (code === 0) {
1606
- resolve(stdout.trim());
1588
+ resolve3(stdout.trim());
1607
1589
  } else {
1608
- resolve(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1590
+ resolve3(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1609
1591
  }
1610
1592
  });
1611
1593
  child.on("error", (err) => {
1612
- resolve(`Error: ${err.message}`);
1594
+ resolve3(`Error: ${err.message}`);
1613
1595
  });
1614
1596
  });
1615
1597
  }
@@ -1875,37 +1857,25 @@ function startWatchdog(deps) {
1875
1857
  }
1876
1858
 
1877
1859
  // src/commands/start.ts
1878
- async function cmdStart(name) {
1879
- if (!name) {
1880
- const agents = listAgents();
1881
- if (agents.length === 0) {
1882
- console.log("No agents configured. Run `elisym init` first.");
1883
- process.exit(1);
1884
- }
1885
- const { default: inquirer } = await import('inquirer');
1886
- const choices = [...agents, { name: "+ Create new agent", value: "__new__" }];
1887
- const { selected } = await inquirer.prompt([
1888
- { type: "list", name: "selected", message: "Select agent to start", choices }
1889
- ]);
1890
- if (selected === "__new__") {
1891
- const { cmdInit: cmdInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1892
- await cmdInit2();
1893
- return;
1894
- }
1895
- name = selected;
1860
+ async function cmdStart(nameArg) {
1861
+ const cwd = process.cwd();
1862
+ const agentName = await resolveStartAgentName(nameArg, cwd);
1863
+ if (!agentName) {
1864
+ return;
1896
1865
  }
1897
- const config = await loadConfigWithPrompt(name);
1866
+ const loaded = await loadAgentWithPrompt(agentName, cwd);
1898
1867
  console.log(`
1899
- Starting agent ${name}...
1868
+ Starting agent ${agentName} (${loaded.source})...
1900
1869
  `);
1901
- let solanaAddress;
1902
- if (config.payments?.length) {
1903
- const solPayment = config.payments.find((p) => p.chain === "solana");
1904
- if (solPayment) {
1905
- solanaAddress = solPayment.address;
1906
- }
1870
+ if (loaded.shadowsGlobal) {
1871
+ console.log(
1872
+ ` ! Using project-local ${agentName} (shadows global agent in ~/.elisym/${agentName}/)
1873
+ `
1874
+ );
1907
1875
  }
1908
- 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";
1909
1879
  if (solanaAddress) {
1910
1880
  try {
1911
1881
  const rpcUrl = getRpcUrl(walletNetwork);
@@ -1921,18 +1891,7 @@ async function cmdStart(name) {
1921
1891
  }
1922
1892
  console.log(` Balance ${formatSol(balance)} (${balance} lamports)`);
1923
1893
  if (balance === 0) {
1924
- if (walletNetwork === "mainnet") {
1925
- console.log(
1926
- " ! Warning: wallet is empty. First incoming payment needs rent-exempt SOL (~0.00089 SOL)."
1927
- );
1928
- } else {
1929
- console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
1930
- }
1931
- }
1932
- if (walletNetwork === "mainnet" && !process.env.SOLANA_RPC_URL) {
1933
- console.log(
1934
- " ! Warning: using public Solana RPC for mainnet. Set SOLANA_RPC_URL for reliable operation."
1935
- );
1894
+ console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
1936
1895
  }
1937
1896
  console.log();
1938
1897
  } catch (e) {
@@ -1940,17 +1899,26 @@ async function cmdStart(name) {
1940
1899
  `);
1941
1900
  }
1942
1901
  }
1943
- if (!config.llm) {
1902
+ if (!loaded.yaml.llm) {
1944
1903
  console.error(" ! No LLM configured. Run `elisym init` to set up LLM.\n");
1945
1904
  process.exit(1);
1946
1905
  }
1947
- 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;
1948
1915
  const allSkills = loadSkillsFromDir(skillsDir);
1949
1916
  if (allSkills.length === 0) {
1950
1917
  console.error(` ! No skills found in ${skillsDir}
1951
1918
  `);
1952
1919
  console.error(" Create a skill directory with a SKILL.md to get started.");
1953
- console.error(" Example: ./skills/my-skill/SKILL.md\n");
1920
+ console.error(` Example: ${skillsDir}/my-skill/SKILL.md
1921
+ `);
1954
1922
  process.exit(1);
1955
1923
  }
1956
1924
  const registry = new SkillRegistry();
@@ -1966,50 +1934,69 @@ async function cmdStart(name) {
1966
1934
  process.exit(1);
1967
1935
  }
1968
1936
  const llm = createLlmClient({
1969
- provider: config.llm.provider,
1970
- apiKey: config.llm.api_key,
1971
- model: config.llm.model,
1972
- 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,
1973
1941
  logUsage: true
1974
1942
  });
1975
1943
  const skillCtx = {
1976
1944
  llm,
1977
- agentName: config.identity.name,
1978
- agentDescription: config.identity.description ?? ""
1945
+ agentName,
1946
+ agentDescription: loaded.yaml.description ?? ""
1979
1947
  };
1980
1948
  console.log(" Connecting to relays and publishing capabilities...");
1981
- const identity = ElisymIdentity.fromHex(config.identity.secret_key);
1982
- 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];
1983
1951
  const client = new ElisymClient({ relays });
1984
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
+ );
1985
1971
  for (const skill of allSkills) {
1986
1972
  if (skill.image || !skill.imageFile) {
1987
1973
  continue;
1988
1974
  }
1989
- try {
1990
- const filePath = join(skillsDir, skill.name, skill.imageFile);
1991
- console.log(` Uploading ${basename(filePath)}...`);
1992
- const data = readFileSync(filePath);
1993
- const blob = new Blob([data]);
1994
- const url = await media.upload(identity, blob, basename(filePath));
1995
- 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) {
1996
1987
  skill.image = url;
1997
- const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
1998
- const mdContent = readFileSync(skillMdPath, "utf-8");
1999
- const updated = mdContent.replace(/^(image_file:\s*.+)$/m, (m) => `${m}
2000
- image: ${url}`);
2001
- writeFileSync(skillMdPath, updated);
2002
- } catch (e) {
2003
- console.warn(` ! Failed to upload image for "${skill.name}": ${e.message}`);
2004
1988
  }
2005
1989
  }
1990
+ if (mediaCacheDirty) {
1991
+ await writeMediaCache(loaded.dir, mediaCache);
1992
+ }
2006
1993
  try {
2007
1994
  await client.discovery.publishProfile(
2008
1995
  identity,
2009
- config.identity.name,
2010
- config.identity.description ?? "",
2011
- config.identity.picture,
2012
- config.identity.banner
1996
+ agentName,
1997
+ loaded.yaml.description ?? "",
1998
+ pictureUrl,
1999
+ bannerUrl
2013
2000
  );
2014
2001
  } catch (e) {
2015
2002
  console.warn(` ! Failed to publish profile: ${e.message}`);
@@ -2075,7 +2062,7 @@ image: ${url}`);
2075
2062
  }, HEARTBEAT_INTERVAL_MS);
2076
2063
  }
2077
2064
  const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
2078
- const ledger = new JobLedger(name);
2065
+ const ledger = new JobLedger(paths.jobs);
2079
2066
  const runtimeConfig = {
2080
2067
  paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
2081
2068
  maxConcurrentJobs: MAX_CONCURRENT_JOBS,
@@ -2114,15 +2101,88 @@ image: ${url}`);
2114
2101
  console.log(" * Running. Press Ctrl+C to stop.\n");
2115
2102
  await runtime.run();
2116
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
+ }
2117
2150
  var MAX_PASSPHRASE_ATTEMPTS = 3;
2118
- 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) {
2119
2179
  const envPassphrase = process.env.ELISYM_PASSPHRASE;
2120
2180
  try {
2121
- return loadConfig(name, envPassphrase);
2181
+ return await loadAgent(name, cwd, envPassphrase);
2122
2182
  } catch (e) {
2123
- const isEncryptedConfigError = /encrypted but no passphrase/i.test(e?.message ?? "");
2183
+ const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2124
2184
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2125
- if (!isEncryptedConfigError && !isWrongPassphrase) {
2185
+ if (!isEncrypted3 && !isWrongPassphrase) {
2126
2186
  throw e;
2127
2187
  }
2128
2188
  if (!process.stdin.isTTY) {
@@ -2140,7 +2200,7 @@ async function loadConfigWithPrompt(name) {
2140
2200
  }
2141
2201
  ]);
2142
2202
  try {
2143
- return loadConfig(name, passphrase);
2203
+ return await loadAgent(name, cwd, passphrase);
2144
2204
  } catch (e) {
2145
2205
  if (!/Decryption failed/i.test(e?.message ?? "")) {
2146
2206
  throw e;
@@ -2155,12 +2215,10 @@ async function loadConfigWithPrompt(name) {
2155
2215
  }
2156
2216
  throw new Error("Unreachable");
2157
2217
  }
2158
-
2159
- // src/commands/wallet.ts
2160
- init_config();
2161
2218
  async function cmdWallet(name) {
2219
+ const cwd = process.cwd();
2162
2220
  if (!name) {
2163
- const agents = listAgents();
2221
+ const agents = await listAgents(cwd);
2164
2222
  if (agents.length === 0) {
2165
2223
  console.error("No agents found.");
2166
2224
  process.exit(1);
@@ -2171,33 +2229,32 @@ async function cmdWallet(name) {
2171
2229
  type: "list",
2172
2230
  name: "selected",
2173
2231
  message: "Select agent:",
2174
- choices: agents
2232
+ choices: agents.map((agent) => ({
2233
+ name: `${agent.name} (${agent.source})`,
2234
+ value: agent.name
2235
+ }))
2175
2236
  }
2176
2237
  ]);
2177
2238
  name = selected;
2178
2239
  }
2179
2240
  const passphrase = process.env.ELISYM_PASSPHRASE;
2180
- const config = loadConfig(name, passphrase);
2181
- 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");
2182
2243
  if (!solPayment?.address) {
2183
2244
  console.error("Solana address not configured for this agent.");
2184
2245
  process.exit(1);
2185
2246
  }
2186
- const network = solPayment.network ?? "devnet";
2187
- const rpcUrl = getRpcUrl(network);
2247
+ const rpcUrl = getRpcUrl(solPayment.network);
2188
2248
  const rpc = createSolanaRpc(rpcUrl);
2189
2249
  const walletAddress = address(solPayment.address);
2190
2250
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2191
2251
  console.log(`
2192
2252
  Agent: ${name}`);
2193
- console.log(` Network: ${network}`);
2253
+ console.log(` Network: ${solPayment.network}`);
2194
2254
  console.log(` Address: ${solPayment.address}`);
2195
2255
  console.log(` Balance: ${formatSol(Number(balance))} (${balance} lamports)
2196
2256
  `);
2197
2257
  }
2198
-
2199
- // src/index.ts
2200
- init_config();
2201
2258
  function readPackageVersion() {
2202
2259
  try {
2203
2260
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2211,50 +2268,50 @@ var PACKAGE_VERSION = readPackageVersion();
2211
2268
 
2212
2269
  // src/index.ts
2213
2270
  process.removeAllListeners("warning");
2214
- var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
2215
- program.command("init").description("Create a new agent (interactive wizard)").action(cmdInit);
2216
- program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(cmdProfile);
2217
- program.command("start [name]").description("Start agent in provider mode").action(cmdStart);
2218
- program.command("list").description("List all agents").action(() => {
2219
- const agents = listAgents();
2220
- if (agents.length === 0) {
2221
- console.log("No agents found. Run `elisym init` to create one.");
2222
- return;
2223
- }
2224
- console.log("\nAgents:");
2225
- for (const name of agents) {
2271
+ function safe(fn) {
2272
+ return async (...args) => {
2226
2273
  try {
2227
- const config = loadConfig(name);
2228
- const secretKey = config.identity.secret_key;
2229
- let npub = "(encrypted)";
2230
- if (!secretKey.startsWith("encrypted:")) {
2231
- const secretBytes = Buffer.from(secretKey, "hex");
2232
- npub = secretBytes.length === 32 ? nip19.npubEncode(getPublicKey(secretBytes)) : "(invalid key)";
2233
- }
2234
- const solAddr = config.payments?.[0]?.address ? ` | Solana: ${config.payments[0].address}` : "";
2235
- console.log(` ${name} | ${npub}${solAddr}`);
2236
- } catch {
2237
- 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);
2238
2278
  }
2239
- }
2240
- console.log();
2241
- });
2242
- program.command("wallet [name]").description("Show wallet balance").action(cmdWallet);
2243
- program.command("delete <name>").description("Delete an agent").action(async (name) => {
2244
- const { default: inquirer } = await import('inquirer');
2245
- const { confirm } = await inquirer.prompt([
2246
- {
2247
- type: "confirm",
2248
- name: "confirm",
2249
- message: `Delete agent "${name}"? This cannot be undone.`,
2250
- 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;
2251
2296
  }
2252
- ]);
2253
- if (confirm) {
2254
- deleteAgent(name);
2255
- console.log(`Agent "${name}" deleted.`);
2256
- }
2257
- });
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));
2258
2315
  program.parse();
2259
2316
  //# sourceMappingURL=index.js.map
2260
2317
  //# sourceMappingURL=index.js.map