akemon 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/DATA_POLICY.md +11 -3
  2. package/README.md +133 -21
  3. package/dist/akemon-home.js +56 -0
  4. package/dist/akemon-message.js +107 -0
  5. package/dist/best-effort.js +8 -0
  6. package/dist/cli.js +1188 -100
  7. package/dist/cognitive-artifact-store.js +101 -0
  8. package/dist/cognitive-event-log.js +47 -0
  9. package/dist/config.js +45 -9
  10. package/dist/context.js +27 -6
  11. package/dist/core/contracts/layers.js +1 -0
  12. package/dist/core/contracts/permission.js +1 -0
  13. package/dist/core/contracts/workspace.js +1 -0
  14. package/dist/core-cognitive-module.js +768 -0
  15. package/dist/engine-peripheral.js +127 -26
  16. package/dist/engine-routing.js +58 -17
  17. package/dist/interactive-session.js +361 -0
  18. package/dist/local-interconnect.js +156 -0
  19. package/dist/local-registry.js +178 -0
  20. package/dist/mcp-server.js +4 -1
  21. package/dist/memory-proposal.js +379 -0
  22. package/dist/memory-recorder.js +368 -0
  23. package/dist/orphan-scan.js +36 -24
  24. package/dist/passive-reflection-cognitive-module.js +172 -0
  25. package/dist/peripheral-registry.js +235 -0
  26. package/dist/permission-audit.js +132 -0
  27. package/dist/relay-client.js +68 -9
  28. package/dist/relay-mode.js +34 -0
  29. package/dist/relay-peripheral.js +139 -49
  30. package/dist/runtime-platform.js +122 -0
  31. package/dist/secretariat/client.js +87 -0
  32. package/dist/self.js +15 -6
  33. package/dist/server.js +3675 -512
  34. package/dist/social-discovery.js +231 -0
  35. package/dist/software-agent-peripheral.js +185 -244
  36. package/dist/software-agent-transport.js +177 -0
  37. package/dist/task-module.js +243 -0
  38. package/dist/task-registry.js +756 -0
  39. package/dist/vendor/xterm/addon-fit.js +2 -0
  40. package/dist/vendor/xterm/addon-search.js +2 -0
  41. package/dist/vendor/xterm/addon-web-links.js +2 -0
  42. package/dist/vendor/xterm/xterm.css +285 -0
  43. package/dist/vendor/xterm/xterm.js +2 -0
  44. package/dist/work-memory.js +59 -15
  45. package/dist/workbench-peripheral-guide.js +79 -0
  46. package/dist/workbench-session.js +1074 -0
  47. package/dist/workbench.html +4011 -0
  48. package/package.json +8 -3
  49. package/scripts/build.cjs +24 -0
  50. package/scripts/check-architecture-baseline.cjs +68 -0
  51. package/scripts/test.cjs +38 -0
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
2
+ import { Command, Option } from "commander";
3
3
  import { serve, onOrderNotify } from "./server.js";
4
4
  import { addAgent } from "./add.js";
5
- import { getOrCreateRelayCredentials } from "./config.js";
5
+ import { getLocalManagerName, getOrCreateLocalOwnerSecret, getOrCreateRelayCredentials, setLocalManagerName, } from "./config.js";
6
6
  import { connectRelay } from "./relay-client.js";
7
7
  import { listAgents } from "./list.js";
8
8
  import { connect } from "./connect.js";
@@ -10,14 +10,22 @@ import { PrivacyFilterUnavailableError, sanitizeText, } from "./privacy-filter.j
10
10
  import { SoftwareAgentStreamCliRenderer } from "./software-agent-stream-cli.js";
11
11
  import { appendWorkMemoryNote, buildWorkMemoryContext, } from "./work-memory.js";
12
12
  import { renderSoftwareAgentRunResult } from "./software-agent-result-cli.js";
13
+ import { DEFAULT_RELAY_HTTP, resolveServeRelayMode, } from "./relay-mode.js";
14
+ import { LocalInstanceLookupError, listRunningLocalInstances, resolveDefaultLocalInstance, resolveLocalInstanceByName, } from "./local-registry.js";
15
+ import { appendLocalPeerContact, createLocalAkemonMessage, } from "./local-interconnect.js";
16
+ import { discoverPublicAkemonProfiles, } from "./social-discovery.js";
17
+ import { getMemoryRecorder, listMemoryRecorders, } from "./memory-recorder.js";
18
+ import { listPermissionAuditRecords, } from "./permission-audit.js";
19
+ import { buildPeripheralExploreBriefing, loadPeripheralRecords, upsertPeripheralRecord, } from "./peripheral-registry.js";
20
+ import { openUrl } from "./runtime-platform.js";
21
+ import { createOwnerChatMessage, SecretariatClient, taskIdFromOwnerChatMessage, taskIsTerminal, } from "./secretariat/client.js";
13
22
  import { readFileSync } from "fs";
14
23
  import { fileURLToPath } from "url";
15
24
  import { dirname, join } from "path";
16
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
26
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
18
- const RELAY_WS = "wss://relay.akemon.dev";
19
- const RELAY_HTTP = "https://relay.akemon.dev";
20
27
  const program = new Command();
28
+ const ALL_MODULES = ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
21
29
  function parsePortOption(port) {
22
30
  const value = typeof port === "number" ? port : parseInt(String(port || "3000"));
23
31
  return Number.isInteger(value) && value > 0 ? value : 3000;
@@ -49,6 +57,57 @@ function parseSoftwareAgentEnvPolicy(value) {
49
57
  console.error("--software-agent-env must be one of: inherit, allowlist");
50
58
  process.exit(1);
51
59
  }
60
+ function parseAkemonMemoryScope(value) {
61
+ const normalized = (value || "task").trim().toLowerCase();
62
+ if (normalized === "none" || normalized === "public" || normalized === "task" || normalized === "owner") {
63
+ return normalized;
64
+ }
65
+ console.error("--memory-scope must be one of: none, public, task, owner");
66
+ process.exit(1);
67
+ }
68
+ function parseMemoryRecorderSource(value) {
69
+ if (value === undefined)
70
+ return undefined;
71
+ const normalized = value.trim().toLowerCase();
72
+ if (normalized === "builtin" || normalized === "module" || normalized === "skill") {
73
+ return normalized;
74
+ }
75
+ console.error("--source must be one of: builtin, module, skill");
76
+ process.exit(1);
77
+ }
78
+ function parseMemoryRecorderScope(value) {
79
+ if (value === undefined)
80
+ return undefined;
81
+ const normalized = value.trim().toLowerCase();
82
+ if (normalized === "self"
83
+ || normalized === "work"
84
+ || normalized === "contacts"
85
+ || normalized === "inbox"
86
+ || normalized === "events"
87
+ || normalized === "conversation"
88
+ || normalized === "product"
89
+ || normalized === "software-agent"
90
+ || normalized === "runtime"
91
+ || normalized === "audit"
92
+ || normalized === "config") {
93
+ return normalized;
94
+ }
95
+ console.error("--scope must be one of: self, work, contacts, inbox, events, conversation, product, software-agent, runtime, audit, config");
96
+ process.exit(1);
97
+ }
98
+ function parsePermissionAuditActionKind(value) {
99
+ if (value === undefined)
100
+ return undefined;
101
+ const normalized = value.trim().toLowerCase();
102
+ if (normalized === "software-agent-task"
103
+ || normalized === "akemon-message"
104
+ || normalized === "relay-publication"
105
+ || normalized === "memory-write") {
106
+ return normalized;
107
+ }
108
+ console.error("--kind must be one of: software-agent-task, akemon-message, relay-publication, memory-write");
109
+ process.exit(1);
110
+ }
52
111
  function parseCommaSeparatedCliOption(value) {
53
112
  if (!value)
54
113
  return undefined;
@@ -108,12 +167,248 @@ function printSoftwareAgentSessionList(sessions) {
108
167
  console.log(` work memory: ${session.workMemoryDir}`);
109
168
  }
110
169
  }
170
+ function printProfileDiscoveryResults(results, query) {
171
+ if (!results.length) {
172
+ console.log(`No public Akemon profiles matched: ${query}`);
173
+ return;
174
+ }
175
+ for (const [index, result] of results.entries()) {
176
+ const profile = result.profile;
177
+ const tags = [...profile.tags, ...profile.interests].slice(0, 8).join(", ");
178
+ const details = [
179
+ profile.status && profile.status !== "unknown" ? profile.status : "",
180
+ profile.engine ? `engine=${profile.engine}` : "",
181
+ tags ? `tags=${tags}` : "",
182
+ `score=${result.score}`,
183
+ ].filter(Boolean).join(" ");
184
+ console.log(`${index + 1}. ${profile.name}${details ? ` ${details}` : ""}`);
185
+ if (profile.description)
186
+ console.log(` ${truncateOneLine(profile.description, 120)}`);
187
+ if (result.reasons.length)
188
+ console.log(` reasons: ${result.reasons.join("; ")}`);
189
+ if (profile.profileUrl)
190
+ console.log(` profile: ${profile.profileUrl}`);
191
+ }
192
+ }
193
+ function printMemoryRecorderList(recorders) {
194
+ if (!recorders.length) {
195
+ console.log("No memory recorders found.");
196
+ return;
197
+ }
198
+ const rows = recorders.map((recorder) => ({
199
+ id: recorder.id,
200
+ source: recorder.source,
201
+ owner: recorder.owner,
202
+ trigger: recorder.trigger.event,
203
+ destinations: summarizeDestinations(recorder),
204
+ approval: recorder.privacy.approvalRequired ? "yes" : "no",
205
+ }));
206
+ const idW = Math.max(2, ...rows.map((row) => row.id.length)) + 2;
207
+ const sourceW = Math.max(6, ...rows.map((row) => row.source.length)) + 2;
208
+ const ownerW = Math.max(5, ...rows.map((row) => row.owner.length)) + 2;
209
+ const triggerW = Math.max(7, ...rows.map((row) => row.trigger.length)) + 2;
210
+ const approvalW = 10;
211
+ console.log(padCli("ID", idW)
212
+ + padCli("SOURCE", sourceW)
213
+ + padCli("OWNER", ownerW)
214
+ + padCli("TRIGGER", triggerW)
215
+ + padCli("APPROVAL", approvalW)
216
+ + "DESTINATIONS");
217
+ for (const row of rows) {
218
+ console.log(padCli(row.id, idW)
219
+ + padCli(row.source, sourceW)
220
+ + padCli(row.owner, ownerW)
221
+ + padCli(row.trigger, triggerW)
222
+ + padCli(row.approval, approvalW)
223
+ + row.destinations);
224
+ }
225
+ }
226
+ function printMemoryRecorderDetail(recorder) {
227
+ console.log(`${recorder.id} - ${recorder.title}`);
228
+ console.log(`Source: ${recorder.source}`);
229
+ console.log(`Owner: ${recorder.owner}`);
230
+ console.log(`Enabled: ${recorder.enabled ? "yes" : "no"}`);
231
+ console.log(`Trigger: ${recorder.trigger.event}`);
232
+ console.log(`Trigger detail: ${recorder.trigger.description}`);
233
+ console.log(`Approval required: ${recorder.privacy.approvalRequired ? "yes" : "no"}`);
234
+ console.log(`Redacts secrets: ${recorder.privacy.redactsSecrets ? "yes" : "no"}`);
235
+ console.log(`Includes owner memory/content: ${recorder.privacy.includesOwnerMemory ? "yes" : "no"}`);
236
+ console.log("Destinations:");
237
+ for (const dest of recorder.destinations) {
238
+ console.log(`- ${dest.scope} ${dest.path}`);
239
+ console.log(` format=${dest.format} mode=${dest.writeMode}`);
240
+ console.log(` ${dest.description}`);
241
+ }
242
+ if (recorder.sourceFiles.length) {
243
+ console.log("Source files:");
244
+ for (const file of recorder.sourceFiles)
245
+ console.log(`- ${file}`);
246
+ }
247
+ if (recorder.notes)
248
+ console.log(`Notes: ${recorder.notes}`);
249
+ }
250
+ function printPermissionAuditRecords(records) {
251
+ if (!records.length) {
252
+ console.log("No permission audit records found.");
253
+ return;
254
+ }
255
+ for (const record of records) {
256
+ const decision = `${record.decision.result}/${record.decision.mode}`;
257
+ const requestedBy = `${record.requestedBy.kind}:${record.requestedBy.id}`;
258
+ const performedBy = `${record.performedBy.kind}:${record.performedBy.id}`;
259
+ const scope = [
260
+ record.riskLevel ? `risk=${record.riskLevel}` : "",
261
+ record.memoryScope ? `memory=${record.memoryScope}` : "",
262
+ record.transport ? `transport=${record.transport}` : "",
263
+ ].filter(Boolean).join(" ");
264
+ console.log(`${record.ts} ${record.actionKind} ${record.action} ${decision}`);
265
+ console.log(` requested: ${requestedBy} performed: ${performedBy}`);
266
+ if (scope)
267
+ console.log(` ${scope}`);
268
+ if (record.references?.taskId)
269
+ console.log(` task: ${record.references.taskId}`);
270
+ if (record.references?.messageId)
271
+ console.log(` message: ${record.references.messageId}`);
272
+ if (record.references?.noteId)
273
+ console.log(` note: ${record.references.noteId}`);
274
+ if (record.workdir)
275
+ console.log(` workdir: ${truncateOneLine(record.workdir, 120)}`);
276
+ if (record.decision.reason)
277
+ console.log(` ${truncateOneLine(record.decision.reason, 140)}`);
278
+ }
279
+ }
280
+ function printPeripheralRecords(records) {
281
+ if (!records.length) {
282
+ console.log("No peripheral records found.");
283
+ return;
284
+ }
285
+ const idWidth = Math.min(Math.max(...records.map((record) => record.id.length), 2), 32);
286
+ const typeWidth = Math.min(Math.max(...records.map((record) => record.type.length), 4), 16);
287
+ console.log(`${padCli("ID", idWidth)} ${padCli("TYPE", typeWidth)} RISK STATUS CAPABILITIES`);
288
+ for (const record of records) {
289
+ console.log([
290
+ padCli(truncateOneLine(record.id, idWidth), idWidth),
291
+ padCli(record.type, typeWidth),
292
+ padCli(record.riskLevel, 7),
293
+ padCli(record.status, 12),
294
+ truncateOneLine(record.capabilities.join(", ") || "-", 80),
295
+ ].join(" "));
296
+ }
297
+ }
298
+ function parsePeripheralRecordType(value) {
299
+ const normalized = (value || "custom").trim().toLowerCase();
300
+ if (normalized === "engine"
301
+ || normalized === "relay"
302
+ || normalized === "software-agent"
303
+ || normalized === "mcp"
304
+ || normalized === "service"
305
+ || normalized === "hardware"
306
+ || normalized === "custom") {
307
+ return normalized;
308
+ }
309
+ console.error("--type must be one of: engine, relay, software-agent, mcp, service, hardware, custom");
310
+ process.exit(1);
311
+ }
312
+ function parsePeripheralRiskLevel(value) {
313
+ const normalized = (value || "medium").trim().toLowerCase();
314
+ if (normalized === "low" || normalized === "medium" || normalized === "high")
315
+ return normalized;
316
+ console.error("--risk must be one of: low, medium, high");
317
+ process.exit(1);
318
+ }
319
+ function parsePeripheralExploreMode(value) {
320
+ const normalized = (value || "none").trim().toLowerCase();
321
+ if (normalized === "none" || normalized === "plain-text")
322
+ return normalized;
323
+ console.error("--explore must be one of: none, plain-text");
324
+ process.exit(1);
325
+ }
326
+ async function resolvePermissionAuditAgentName(value) {
327
+ const explicit = value?.trim();
328
+ if (explicit)
329
+ return explicit;
330
+ const manager = await getLocalManagerName();
331
+ if (manager)
332
+ return manager;
333
+ console.error("No Akemon name specified for audit. Use --name <name> or set a local manager.");
334
+ process.exit(1);
335
+ }
336
+ async function resolvePeripheralRegistryAgentName(value) {
337
+ const explicit = value?.trim();
338
+ if (explicit)
339
+ return explicit;
340
+ const manager = await getLocalManagerName();
341
+ if (manager)
342
+ return manager;
343
+ console.error("No Akemon name specified for peripherals. Use --name <name> or set a local manager.");
344
+ process.exit(1);
345
+ }
346
+ async function runPermissionAuditListCli(opts) {
347
+ const decision = opts.decision === undefined ? undefined : String(opts.decision).trim().toLowerCase();
348
+ if (decision !== undefined && decision !== "allowed" && decision !== "denied") {
349
+ console.error("--decision must be one of: allowed, denied");
350
+ process.exit(1);
351
+ }
352
+ const agentName = await resolvePermissionAuditAgentName(opts.name);
353
+ const records = await listPermissionAuditRecords(agentName, {
354
+ limit: clampPositiveInt(opts.limit, 20, 500),
355
+ actionKind: parsePermissionAuditActionKind(opts.kind),
356
+ decision: decision,
357
+ });
358
+ if (opts.json) {
359
+ console.log(JSON.stringify(records, null, 2));
360
+ return;
361
+ }
362
+ printPermissionAuditRecords(records);
363
+ }
364
+ function summarizeDestinations(recorder) {
365
+ return recorder.destinations
366
+ .map((dest) => `${dest.scope}:${dest.path}`)
367
+ .join("; ");
368
+ }
369
+ function padCli(value, width) {
370
+ return value.padEnd(width);
371
+ }
372
+ async function fetchPublicRelayAgentsForDiscovery(relayUrl) {
373
+ const baseUrl = relayUrl.replace(/\/+$/, "");
374
+ const url = `${baseUrl}/v1/agents?online=true&public=true`;
375
+ let res;
376
+ try {
377
+ res = await fetch(url);
378
+ }
379
+ catch (error) {
380
+ throw new Error(`Failed to connect to relay: ${error instanceof Error ? error.message : String(error)}`);
381
+ }
382
+ if (!res.ok)
383
+ throw new Error(`Failed to fetch public agents: HTTP ${res.status}`);
384
+ const data = await res.json();
385
+ if (!Array.isArray(data))
386
+ throw new Error("Relay returned an invalid agent list");
387
+ return data;
388
+ }
111
389
  function truncateOneLine(value, max) {
112
390
  const oneLine = value.replace(/\s+/g, " ").trim();
113
391
  if (oneLine.length <= max)
114
392
  return oneLine;
115
393
  return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
116
394
  }
395
+ function formatLocalInstanceRecord(record) {
396
+ return `${record.name} port=${record.port} pid=${record.pid} mode=${record.mode} workdir=${record.workdir}`;
397
+ }
398
+ function printLocalInstanceCandidates(candidates) {
399
+ for (const candidate of candidates) {
400
+ console.error(` ${formatLocalInstanceRecord(candidate)}`);
401
+ }
402
+ }
403
+ function printLocalInstanceList(instances) {
404
+ if (instances.length === 0) {
405
+ console.log("No running local Akemon instances.");
406
+ return;
407
+ }
408
+ for (const instance of instances) {
409
+ console.log(formatLocalInstanceRecord(instance));
410
+ }
411
+ }
117
412
  async function callLocalOwnerEndpoint(path, opts, init = {}) {
118
413
  const res = await fetchLocalOwnerEndpoint(path, opts, init);
119
414
  const text = await res.text();
@@ -131,10 +426,10 @@ async function callLocalOwnerEndpoint(path, opts, init = {}) {
131
426
  return data;
132
427
  }
133
428
  async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
134
- const credentials = await getOrCreateRelayCredentials();
135
- const port = parsePortOption(opts.port);
429
+ const secretKey = await getOrCreateLocalOwnerSecret();
430
+ const port = await resolveLocalOwnerPort(opts);
136
431
  const headers = {
137
- Authorization: `Bearer ${credentials.secretKey}`,
432
+ Authorization: `Bearer ${secretKey}`,
138
433
  };
139
434
  if (init.body !== undefined)
140
435
  headers["Content-Type"] = "application/json";
@@ -155,13 +450,51 @@ async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
155
450
  process.exit(1);
156
451
  }
157
452
  if (error instanceof TypeError && error.message === "fetch failed") {
158
- console.error(`Cannot connect to local akemon serve on port ${port}. Start it with: akemon serve --port ${port}`);
453
+ console.error(`Cannot connect to local Akemon on port ${port}. Start it with: akemon run --port ${port}`);
159
454
  process.exit(1);
160
455
  }
161
456
  throw error;
162
457
  }
163
458
  return res;
164
459
  }
460
+ async function resolveLocalOwnerPort(opts) {
461
+ if (opts.port)
462
+ return parsePortOption(opts.port);
463
+ if (opts.name) {
464
+ try {
465
+ return (await resolveLocalInstanceByName(opts.name)).port;
466
+ }
467
+ catch (error) {
468
+ if (error instanceof LocalInstanceLookupError) {
469
+ console.error(error.message);
470
+ for (const candidate of error.candidates) {
471
+ console.error(` ${candidate.name}: port=${candidate.port} pid=${candidate.pid} workdir=${candidate.workdir}`);
472
+ }
473
+ process.exit(1);
474
+ }
475
+ throw error;
476
+ }
477
+ }
478
+ try {
479
+ return (await resolveDefaultLocalInstance()).port;
480
+ }
481
+ catch (error) {
482
+ if (error instanceof LocalInstanceLookupError) {
483
+ if (error.code === "ambiguous") {
484
+ console.error(error.message);
485
+ printLocalInstanceCandidates(error.candidates);
486
+ process.exit(1);
487
+ }
488
+ return parsePortOption(undefined);
489
+ }
490
+ throw error;
491
+ }
492
+ return parsePortOption(undefined);
493
+ }
494
+ async function resolveLocalOwnerEndpointOptions(opts) {
495
+ const port = await resolveLocalOwnerPort(opts);
496
+ return { ...opts, port: String(port) };
497
+ }
165
498
  async function streamLocalOwnerEndpoint(path, opts, body) {
166
499
  const res = await fetchLocalOwnerEndpoint(path, opts, {
167
500
  method: "POST",
@@ -259,55 +592,370 @@ async function runSoftwareAgentCli(goalParts, opts, forcedSessionId) {
259
592
  if (failed)
260
593
  process.exit(1);
261
594
  }
262
- program
263
- .name("akemon")
264
- .description("Local AI companion runtime with memory, modules, relay sync, and software-agent control")
265
- .version(pkg.version);
266
- program
267
- .command("serve")
268
- .description("Publish your agent to the akemon relay")
269
- .option("-p, --port <port>", "Local port for MCP loopback", "3000")
270
- .option("-w, --workdir <path>", "Working directory for the engine (default: cwd)")
271
- .option("-n, --name <name>", "Agent name", "my-agent")
272
- .option("-m, --model <model>", "Model to use (e.g. claude-sonnet-4-6, gpt-4o)")
273
- .option("--engine <engine>", "Engine: claude, codex, opencode, gemini, raw, human, or any CLI", "claude")
274
- .option("--desc <description>", "Agent description (for discovery)")
275
- .option("--tags <tags>", "Comma-separated tags (e.g. vue,frontend,review)")
276
- .option("--public", "Allow anyone to call this agent without a key")
277
- .option("--max-tasks <n>", "Maximum tasks per day (PP)")
278
- .option("--approve", "Review every task before execution")
279
- .option("--mock", "Use mock responses (for demo/testing)")
280
- .option("--allow-all", "Skip all permission prompts (for self-use)")
281
- .option("--price <n>", "Price in credits per call (default: 1)", "1")
282
- .option("--mcp-server <command>", "Wrap a community MCP server (stdio) and expose its tools via relay")
283
- .option("--avatar <url>", "Custom avatar URL (default: auto-generated from name)")
284
- .option("--notify <url>", "ntfy.sh topic URL for push notifications (e.g. https://ntfy.sh/my-agent)")
285
- .option("--interval <minutes>", "Consciousness cycle interval in minutes (default: 1440 = 24h)")
286
- .option("--with <modules>", "Enable specific modules (comma-separated: biostate,memory)")
287
- .option("--without <modules>", "Disable specific modules (comma-separated: biostate,memory)")
288
- .option("--script <name>", "Script to load for ScriptModule (default: daily-life)", "daily-life")
289
- .option("--terminal", "Enable remote terminal access (PTY)")
290
- .option("--software-agent-env <policy>", "Software-agent child environment policy: inherit or allowlist", process.env.AKEMON_SOFTWARE_AGENT_ENV_POLICY || "inherit")
291
- .option("--software-agent-env-allow <vars>", "Comma-separated extra env vars for software-agent allowlist")
292
- .option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
293
- .action(async (opts) => {
294
- const port = parseInt(opts.port);
295
- const engine = opts.engine || "claude";
296
- // Connect to relay
297
- const credentials = await getOrCreateRelayCredentials();
298
- // Derive relay HTTP URL from WS URL
299
- const relayWs = opts.relay;
300
- const relayHttp = relayWs.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
301
- // Parse module selection
302
- const ALL_MODULES = ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
303
- let enabledModules;
595
+ async function runWorkbenchStartCli(tool, args, opts) {
596
+ const body = { tool };
597
+ if (opts.command)
598
+ body.command = opts.command;
599
+ if (args.length > 0)
600
+ body.args = args;
601
+ if (opts.workdir)
602
+ body.workdir = opts.workdir;
603
+ if (opts.allowOutsideWorkdir)
604
+ body.allowOutsideWorkdir = true;
605
+ if (opts.label)
606
+ body.label = opts.label;
607
+ if (opts.input)
608
+ body.initialInput = opts.input;
609
+ const cols = parsePositiveIntCliOption(opts.cols, "--cols");
610
+ const rows = parsePositiveIntCliOption(opts.rows, "--rows");
611
+ if (cols !== undefined)
612
+ body.cols = cols;
613
+ if (rows !== undefined)
614
+ body.rows = rows;
615
+ const data = await callLocalOwnerEndpoint("/self/workbench/sessions", opts, {
616
+ method: "POST",
617
+ body: JSON.stringify(body),
618
+ });
619
+ if (opts.json) {
620
+ console.log(JSON.stringify(data.session, null, 2));
621
+ return;
622
+ }
623
+ printWorkbenchSession(data.session);
624
+ console.error(`[workbench] next: akemon workbench-tail ${data.session.sessionId} | akemon workbench-input ${data.session.sessionId} "..." | akemon workbench-stop ${data.session.sessionId}`);
625
+ }
626
+ function printWorkbenchSession(session) {
627
+ const status = session.status || "unknown";
628
+ const command = session.commandLineDisplay || "";
629
+ const started = session.startedAt || "-";
630
+ const workdir = session.workdir || "-";
631
+ console.log(`${session.sessionId} ${status} ${session.tool || "custom"} ${started}`);
632
+ if (command)
633
+ console.log(` command: ${command}`);
634
+ console.log(` workdir: ${workdir}`);
635
+ if (session.logPath)
636
+ console.log(` log: ${session.logPath}`);
637
+ if (typeof session.ownerDirectInputCount === "number") {
638
+ console.log(` owner-direct inputs: ${session.ownerDirectInputCount}`);
639
+ }
640
+ }
641
+ async function listWorkbenchSessionsCli(opts) {
642
+ const data = await callLocalOwnerEndpoint("/self/workbench/sessions", opts, { method: "GET" });
643
+ const sessions = Array.isArray(data.sessions) ? data.sessions : [];
644
+ if (opts.json) {
645
+ console.log(JSON.stringify(sessions, null, 2));
646
+ return;
647
+ }
648
+ if (!sessions.length) {
649
+ console.log("No Workbench sessions found.");
650
+ return;
651
+ }
652
+ for (const session of sessions)
653
+ printWorkbenchSession(session);
654
+ }
655
+ async function showWorkbenchTailCli(sessionId, opts) {
656
+ const data = await callLocalOwnerEndpoint(`/self/workbench/sessions/${encodeURIComponent(sessionId)}/tail`, opts, { method: "GET" });
657
+ if (opts.json) {
658
+ console.log(JSON.stringify(data, null, 2));
659
+ return;
660
+ }
661
+ process.stdout.write(String(data.tail || ""));
662
+ }
663
+ async function sendWorkbenchInputCli(sessionId, inputParts, opts) {
664
+ const inputText = inputParts.join(" ");
665
+ if (!inputText) {
666
+ console.error("Missing input text");
667
+ process.exit(1);
668
+ }
669
+ const input = opts.enter === false || inputText.endsWith("\n") ? inputText : `${inputText}\n`;
670
+ const data = await callLocalOwnerEndpoint(`/self/workbench/sessions/${encodeURIComponent(sessionId)}/input`, opts, {
671
+ method: "POST",
672
+ body: JSON.stringify({ input }),
673
+ });
674
+ if (opts.json) {
675
+ console.log(JSON.stringify(data.session, null, 2));
676
+ return;
677
+ }
678
+ printWorkbenchSession(data.session);
679
+ }
680
+ async function stopWorkbenchSessionCli(sessionId, opts) {
681
+ const data = await callLocalOwnerEndpoint(`/self/workbench/sessions/${encodeURIComponent(sessionId)}/stop`, opts, {
682
+ method: "POST",
683
+ });
684
+ if (opts.json) {
685
+ console.log(JSON.stringify(data.session, null, 2));
686
+ return;
687
+ }
688
+ printWorkbenchSession(data.session);
689
+ }
690
+ async function resizeWorkbenchSessionCli(sessionId, opts) {
691
+ const cols = parsePositiveIntCliOption(opts.cols, "--cols");
692
+ const rows = parsePositiveIntCliOption(opts.rows, "--rows");
693
+ if (cols === undefined || rows === undefined) {
694
+ console.error("--cols and --rows are required");
695
+ process.exit(1);
696
+ }
697
+ const data = await callLocalOwnerEndpoint(`/self/workbench/sessions/${encodeURIComponent(sessionId)}/resize`, opts, {
698
+ method: "POST",
699
+ body: JSON.stringify({ cols, rows }),
700
+ });
701
+ if (opts.json) {
702
+ console.log(JSON.stringify(data.session, null, 2));
703
+ return;
704
+ }
705
+ printWorkbenchSession(data.session);
706
+ }
707
+ async function openWorkbenchUiCli(opts) {
708
+ const secretKey = await getOrCreateLocalOwnerSecret();
709
+ const port = await resolveLocalOwnerPort(opts);
710
+ const url = `http://127.0.0.1:${port}/workbench#token=${encodeURIComponent(secretKey)}`;
711
+ console.log(`Workbench: ${url}`);
712
+ if (opts.open === false || opts.printUrl)
713
+ return;
714
+ try {
715
+ await openUrl(url);
716
+ }
717
+ catch (error) {
718
+ console.error(error instanceof Error ? error.message : String(error));
719
+ process.exit(1);
720
+ }
721
+ }
722
+ function createSecretariatClient(opts) {
723
+ return new SecretariatClient({
724
+ requestJson: (path, init) => callLocalOwnerEndpoint(path, opts, init),
725
+ });
726
+ }
727
+ async function resolveSecretariatAgentName(opts) {
728
+ if (opts.name && !opts.port)
729
+ return String(opts.name);
730
+ const data = await callLocalOwnerEndpoint("/self/state", opts, { method: "GET" });
731
+ const agent = typeof data.agent === "string" ? data.agent.trim() : "";
732
+ if (!agent) {
733
+ console.error("Could not resolve the target Akemon name. Pass --name when using --port.");
734
+ process.exit(1);
735
+ }
736
+ return agent;
737
+ }
738
+ function defaultChatConversationId(opts) {
739
+ const value = typeof opts.conversation === "string" ? opts.conversation.trim() : "";
740
+ return value || "akemon_cli";
741
+ }
742
+ function printSecretariatMessageResponse(data, opts) {
743
+ if (opts.json) {
744
+ console.log(JSON.stringify(data, null, 2));
745
+ return;
746
+ }
747
+ const output = data.output || data.response?.payload?.text || "";
748
+ if (output)
749
+ console.log(output);
750
+ if (data.task?.taskId) {
751
+ console.error(`[akemon-chat] task: ${data.task.taskId}`);
752
+ }
753
+ }
754
+ function printSecretariatTaskStatus(task) {
755
+ const view = task.view;
756
+ const stage = view?.stage?.label || task.stage;
757
+ const status = [task.status, task.stage].filter(Boolean).join("/");
758
+ console.log(`${task.taskId} ${status} ${stage}`);
759
+ if (task.title)
760
+ console.log(` ${truncateOneLine(task.title, 120)}`);
761
+ if (task.summary)
762
+ console.log(` ${truncateOneLine(task.summary, 160)}`);
763
+ if (task.conversationId)
764
+ console.log(` conversation: ${task.conversationId}`);
765
+ if (task.updatedAt)
766
+ console.log(` updated: ${task.updatedAt}`);
767
+ }
768
+ function taskReportText(response) {
769
+ const task = response.task;
770
+ const view = response.taskView || task.view;
771
+ const latestReview = Array.isArray(view?.reviews) && view.reviews.length
772
+ ? view.reviews[view.reviews.length - 1]
773
+ : null;
774
+ return view?.report?.text
775
+ || latestReview?.reportText
776
+ || (typeof task.data?.reportText === "string" ? task.data.reportText : "")
777
+ || task.summary
778
+ || task.nextAction
779
+ || "";
780
+ }
781
+ function printSecretariatTaskReport(response, opts) {
782
+ if (opts.json) {
783
+ console.log(JSON.stringify(response, null, 2));
784
+ return;
785
+ }
786
+ const text = taskReportText(response);
787
+ if (text) {
788
+ console.log(text);
789
+ return;
790
+ }
791
+ printSecretariatTaskStatus(response.task);
792
+ }
793
+ async function runChatSendCli(textParts, opts) {
794
+ const text = textParts.join(" ").trim();
795
+ if (!text) {
796
+ console.error("Message text is required");
797
+ process.exit(1);
798
+ }
799
+ const targetOpts = await resolveLocalOwnerEndpointOptions(opts);
800
+ const targetAgent = await resolveSecretariatAgentName(targetOpts);
801
+ const conversationId = defaultChatConversationId(opts);
802
+ const createdAt = opts.createdAt || new Date().toISOString();
803
+ const message = createOwnerChatMessage({
804
+ targetAgent,
805
+ text,
806
+ conversationId,
807
+ memoryScope: parseAkemonMemoryScope(opts.memoryScope),
808
+ id: opts.messageId,
809
+ createdAt,
810
+ });
811
+ const taskId = taskIdFromOwnerChatMessage(message);
812
+ const client = createSecretariatClient(targetOpts);
813
+ if (opts.wait === false) {
814
+ const data = await client.submitOwnerIntent({ message, wait: false });
815
+ const acceptedTaskId = data.task?.taskId || taskId;
816
+ if (opts.json) {
817
+ console.log(JSON.stringify({
818
+ ...data,
819
+ submitted: true,
820
+ wait: false,
821
+ taskId: acceptedTaskId,
822
+ messageId: message.id,
823
+ conversationId,
824
+ }, null, 2));
825
+ return;
826
+ }
827
+ console.log(`Submitted: ${acceptedTaskId}`);
828
+ console.log(`Status: akemon task status ${acceptedTaskId} --port ${targetOpts.port}`);
829
+ console.log(`Report: akemon task report ${acceptedTaskId} --port ${targetOpts.port}`);
830
+ return;
831
+ }
832
+ const data = await client.submitOwnerIntent({ message });
833
+ printSecretariatMessageResponse(data, opts);
834
+ }
835
+ async function runChatFollowCli(taskId, textParts, opts) {
836
+ const text = textParts.join(" ").trim();
837
+ if (!taskId || !text) {
838
+ console.error("Usage: akemon chat follow <taskId> <message...>");
839
+ process.exit(1);
840
+ }
841
+ const targetOpts = await resolveLocalOwnerEndpointOptions(opts);
842
+ const client = createSecretariatClient(targetOpts);
843
+ const [targetAgent, taskResponse] = await Promise.all([
844
+ resolveSecretariatAgentName(targetOpts),
845
+ client.getTaskStatus(taskId).catch(() => null),
846
+ ]);
847
+ const conversationId = opts.conversation || taskResponse?.task.conversationId || defaultChatConversationId(opts);
848
+ const message = createOwnerChatMessage({
849
+ targetAgent,
850
+ text,
851
+ conversationId,
852
+ memoryScope: parseAkemonMemoryScope(opts.memoryScope),
853
+ });
854
+ const data = await client.queueOwnerFollowUp({ taskId, message });
855
+ printSecretariatMessageResponse(data, opts);
856
+ }
857
+ async function runTaskStatusCli(taskId, opts) {
858
+ const targetOpts = await resolveLocalOwnerEndpointOptions(opts);
859
+ const data = await createSecretariatClient(targetOpts).getTaskStatus(taskId);
860
+ if (opts.json) {
861
+ console.log(JSON.stringify(data, null, 2));
862
+ return;
863
+ }
864
+ printSecretariatTaskStatus(data.task);
865
+ }
866
+ async function runTaskReportCli(taskId, opts) {
867
+ const targetOpts = await resolveLocalOwnerEndpointOptions(opts);
868
+ const data = await createSecretariatClient(targetOpts).getTaskReport(taskId);
869
+ printSecretariatTaskReport(data, opts);
870
+ }
871
+ async function runTaskWatchCli(taskId, opts) {
872
+ const targetOpts = await resolveLocalOwnerEndpointOptions(opts);
873
+ const client = createSecretariatClient(targetOpts);
874
+ const intervalMs = parsePositiveIntCliOption(opts.intervalMs, "--interval-ms") || 1000;
875
+ let lastKey = "";
876
+ for (;;) {
877
+ const data = await client.getTaskStatus(taskId);
878
+ const task = data.task;
879
+ const key = [task.status, task.stage, task.updatedAt, task.summary || ""].join("|");
880
+ if (key !== lastKey) {
881
+ lastKey = key;
882
+ if (opts.jsonl) {
883
+ console.log(JSON.stringify({
884
+ taskId: task.taskId,
885
+ status: task.status,
886
+ stage: task.stage,
887
+ updatedAt: task.updatedAt,
888
+ summary: task.summary,
889
+ report: taskReportText(data) || undefined,
890
+ }));
891
+ }
892
+ else {
893
+ printSecretariatTaskStatus(task);
894
+ if (taskIsTerminal(task)) {
895
+ const report = taskReportText(data);
896
+ if (report)
897
+ console.log(` report: ${truncateOneLine(report, 180)}`);
898
+ }
899
+ }
900
+ }
901
+ if (taskIsTerminal(task) || opts.once)
902
+ break;
903
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
904
+ }
905
+ }
906
+ function addLocalRuntimeOptions(command) {
907
+ return command
908
+ .option("-p, --port <port>", "Local port for MCP loopback", "3000")
909
+ .option("-w, --workdir <path>", "Working directory for the engine (default: cwd)")
910
+ .option("-n, --name <name>", "Agent name", "my-agent")
911
+ .option("-m, --model <model>", "Model to use (e.g. claude-sonnet-4-6, gpt-4o)")
912
+ .option("--engine <engine>", "Engine: claude, codex, opencode, gemini, raw, human, or any CLI", "claude")
913
+ .option("--approve", "Review every task before execution")
914
+ .option("--mock", "Use mock responses (for demo/testing)")
915
+ .option("--allow-all", "Skip all permission prompts (for self-use)")
916
+ .option("--mcp-server <command>", "Wrap a community MCP server (stdio) and expose its tools")
917
+ .option("--notify <url>", "ntfy.sh topic URL for push notifications (e.g. https://ntfy.sh/my-agent)")
918
+ .option("--interval <minutes>", "Consciousness cycle interval in minutes (default: 1440 = 24h)")
919
+ .option("--with <modules>", "Enable specific modules (comma-separated: biostate,memory)")
920
+ .option("--without <modules>", "Disable specific modules (comma-separated: biostate,memory)")
921
+ .option("--script <name>", "Script to load for ScriptModule (default: daily-life)", "daily-life")
922
+ .option("--software-agent-env <policy>", "Software-agent child environment policy: inherit or allowlist", process.env.AKEMON_SOFTWARE_AGENT_ENV_POLICY || "inherit")
923
+ .option("--software-agent-env-allow <vars>", "Comma-separated extra env vars for software-agent allowlist");
924
+ }
925
+ function addLocalAkemonSelectionOptions(command) {
926
+ return command
927
+ .option("-n, --name <name>", "Running local Akemon name")
928
+ .option("-p, --port <port>", "Local Akemon port (overrides --name)");
929
+ }
930
+ function parseEnabledModules(opts) {
304
931
  if (opts.with) {
305
- enabledModules = opts.with.split(",").map((m) => m.trim());
932
+ return opts.with.split(",").map((m) => m.trim()).filter(Boolean);
933
+ }
934
+ if (opts.without) {
935
+ const disabled = opts.without.split(",").map((m) => m.trim()).filter(Boolean);
936
+ return ALL_MODULES.filter((m) => !disabled.includes(m));
306
937
  }
307
- else if (opts.without) {
308
- const disabled = opts.without.split(",").map((m) => m.trim());
309
- enabledModules = ALL_MODULES.filter(m => !disabled.includes(m));
938
+ return undefined;
939
+ }
940
+ async function startServeCli(opts, config = {}) {
941
+ const port = parsePortOption(opts.port);
942
+ const engine = opts.engine || "claude";
943
+ let relayMode;
944
+ try {
945
+ relayMode = resolveServeRelayMode({
946
+ localOnly: config.forceLocalOnly || opts.localOnly,
947
+ public: opts.public,
948
+ relay: opts.relay,
949
+ });
310
950
  }
951
+ catch (error) {
952
+ console.error(error instanceof Error ? error.message : String(error));
953
+ process.exit(1);
954
+ }
955
+ let relayCredentials = null;
956
+ const secretKey = relayMode.enabled
957
+ ? (relayCredentials = await getOrCreateRelayCredentials()).secretKey
958
+ : await getOrCreateLocalOwnerSecret();
311
959
  serve({
312
960
  port,
313
961
  workdir: opts.workdir,
@@ -317,37 +965,65 @@ program
317
965
  approve: opts.approve,
318
966
  allowAll: opts.allowAll,
319
967
  engine,
320
- relayHttp,
321
- secretKey: credentials.secretKey,
968
+ relayHttp: relayMode.relayHttp,
969
+ secretKey,
322
970
  mcpServer: opts.mcpServer,
323
971
  cycleInterval: opts.interval ? parseInt(opts.interval) : undefined,
324
972
  notifyUrl: opts.notify,
325
- enabledModules,
973
+ enabledModules: parseEnabledModules(opts),
326
974
  scriptName: opts.script,
327
975
  softwareAgentEnvPolicy: parseSoftwareAgentEnvPolicy(opts.softwareAgentEnv),
328
976
  softwareAgentEnvAllowlist: parseCommaSeparatedCliOption(opts.softwareAgentEnvAllow),
329
977
  });
330
978
  console.log(`\nakemon v${pkg.version}`);
331
- if (!opts.public) {
332
- console.log(`Access key: ${credentials.accessKey} (share with publishers)`);
333
- }
334
- console.log(`Relay: ${relayWs}\n`);
335
- // Default avatar: DiceBear bottts-neutral (deterministic from name)
336
- const avatar = opts.avatar || `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${encodeURIComponent(opts.name)}`;
337
- connectRelay({
338
- relayUrl: relayWs,
339
- agentName: opts.name,
340
- credentials,
341
- localPort: port,
342
- description: opts.desc,
343
- isPublic: opts.public,
344
- engine,
345
- tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
346
- price: parseInt(opts.price) || 1,
347
- avatar,
348
- onOrderNotify,
349
- enableTerminal: opts.terminal,
350
- });
979
+ console.log(`Mode: ${relayMode.enabled ? "relay" : "local-only"}`);
980
+ if (relayMode.enabled && relayCredentials && !opts.public) {
981
+ console.log(`Access key: ${relayCredentials.accessKey} (share with publishers)`);
982
+ }
983
+ console.log(`Relay: ${relayMode.relayWs || "disabled"}\n`);
984
+ if (relayMode.enabled && relayMode.relayWs && relayCredentials) {
985
+ // Default avatar: DiceBear bottts-neutral (deterministic from name)
986
+ const avatar = opts.avatar || `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${encodeURIComponent(opts.name)}`;
987
+ connectRelay({
988
+ relayUrl: relayMode.relayWs,
989
+ agentName: opts.name,
990
+ credentials: relayCredentials,
991
+ localPort: port,
992
+ description: opts.desc,
993
+ isPublic: opts.public,
994
+ engine,
995
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
996
+ price: parseInt(opts.price) || 1,
997
+ avatar,
998
+ onOrderNotify,
999
+ enableTerminal: opts.terminal,
1000
+ });
1001
+ }
1002
+ }
1003
+ program
1004
+ .name("akemon")
1005
+ .description("Local AI companion runtime with memory, modules, relay sync, and software-agent control")
1006
+ .version(pkg.version);
1007
+ addLocalRuntimeOptions(program
1008
+ .command("run")
1009
+ .description("Run a named local Akemon without relay"))
1010
+ .action(async (opts) => {
1011
+ await startServeCli(opts, { forceLocalOnly: true });
1012
+ });
1013
+ addLocalRuntimeOptions(program
1014
+ .command("serve")
1015
+ .description("Run local Akemon; optionally connect or publish to relay"))
1016
+ .option("--desc <description>", "Agent description (for discovery)")
1017
+ .option("--tags <tags>", "Comma-separated tags (e.g. vue,frontend,review)")
1018
+ .option("--public", "Allow anyone to call this agent without a key")
1019
+ .option("--max-tasks <n>", "Maximum tasks per day (PP)")
1020
+ .option("--price <n>", "Price in credits per call (default: 1)", "1")
1021
+ .option("--avatar <url>", "Custom avatar URL (default: auto-generated from name)")
1022
+ .option("--terminal", "Enable remote terminal access (PTY)")
1023
+ .option("--local-only", "Force local-only mode and disable relay")
1024
+ .option("--relay <url>", "Relay WebSocket URL (enables relay; --public uses the default relay)")
1025
+ .action(async (opts) => {
1026
+ await startServeCli(opts);
351
1027
  });
352
1028
  program
353
1029
  .command("add")
@@ -362,7 +1038,7 @@ program
362
1038
  await addAgent(name, endpoint, opts.key, platform);
363
1039
  }
364
1040
  else {
365
- const relayEndpoint = `${RELAY_HTTP}/v1/agent/${name}/mcp`;
1041
+ const relayEndpoint = `${DEFAULT_RELAY_HTTP}/v1/agent/${name}/mcp`;
366
1042
  await addAgent(name, relayEndpoint, opts.key, platform);
367
1043
  }
368
1044
  });
@@ -371,21 +1047,196 @@ program
371
1047
  .description("List available agents on the relay")
372
1048
  .option("--search <query>", "Filter by name or description")
373
1049
  .action(async (opts) => {
374
- await listAgents(RELAY_HTTP, opts.search);
1050
+ await listAgents(DEFAULT_RELAY_HTTP, opts.search);
1051
+ });
1052
+ program
1053
+ .command("discover")
1054
+ .description("Discover public Akemon profiles by public relay interests")
1055
+ .argument("<query...>", "Interest or profile query")
1056
+ .option("--relay <url>", "Relay HTTP URL", DEFAULT_RELAY_HTTP)
1057
+ .option("-l, --limit <n>", "Maximum candidates to print", "5")
1058
+ .option("--exclude <name>", "Exclude one Akemon name from results")
1059
+ .option("--json", "Print raw JSON")
1060
+ .action(async (queryParts, opts) => {
1061
+ const query = queryParts.join(" ").trim();
1062
+ if (!query) {
1063
+ console.error("Discovery query is required");
1064
+ process.exit(1);
1065
+ }
1066
+ try {
1067
+ const rawProfiles = await fetchPublicRelayAgentsForDiscovery(opts.relay);
1068
+ const results = discoverPublicAkemonProfiles(rawProfiles, query, {
1069
+ assumePublic: true,
1070
+ source: "relay",
1071
+ limit: clampPositiveInt(opts.limit, 5, 50),
1072
+ exclude: opts.exclude,
1073
+ });
1074
+ if (opts.json) {
1075
+ console.log(JSON.stringify({ query, results }, null, 2));
1076
+ return;
1077
+ }
1078
+ printProfileDiscoveryResults(results, query);
1079
+ }
1080
+ catch (error) {
1081
+ console.error(error instanceof Error ? error.message : String(error));
1082
+ process.exit(1);
1083
+ }
375
1084
  });
376
1085
  program
377
1086
  .command("connect")
378
1087
  .description("Connect to the akemon network as a client (stdio MCP server for OpenClaw, Claude, etc.)")
379
- .option("--relay <url>", "Relay HTTP URL", RELAY_HTTP)
1088
+ .option("--relay <url>", "Relay HTTP URL", DEFAULT_RELAY_HTTP)
380
1089
  .option("--key <key>", "Access key for calling private agents")
381
1090
  .action(async (opts) => {
382
1091
  await connect({ relay: opts.relay, key: opts.key });
383
1092
  });
1093
+ program
1094
+ .command("manager")
1095
+ .description("Show or set the default local manager Akemon")
1096
+ .argument("[name]", "Local Akemon name to use as manager")
1097
+ .action(async (name) => {
1098
+ try {
1099
+ if (name) {
1100
+ const manager = await setLocalManagerName(name);
1101
+ console.log(`Local manager: ${manager}`);
1102
+ return;
1103
+ }
1104
+ const manager = await getLocalManagerName();
1105
+ console.log(manager ? `Local manager: ${manager}` : "No local manager set.");
1106
+ }
1107
+ catch (error) {
1108
+ console.error(error instanceof Error ? error.message : String(error));
1109
+ process.exit(1);
1110
+ }
1111
+ });
1112
+ program
1113
+ .command("instances")
1114
+ .description("List running local Akemon instances")
1115
+ .option("--json", "Print raw JSON")
1116
+ .action(async (opts) => {
1117
+ try {
1118
+ const instances = await listRunningLocalInstances();
1119
+ if (opts.json) {
1120
+ console.log(JSON.stringify({ instances }, null, 2));
1121
+ return;
1122
+ }
1123
+ printLocalInstanceList(instances);
1124
+ }
1125
+ catch (error) {
1126
+ console.error(error instanceof Error ? error.message : String(error));
1127
+ process.exit(1);
1128
+ }
1129
+ });
1130
+ program
1131
+ .command("message")
1132
+ .description("Send a structured local Akemon message to another running Akemon")
1133
+ .argument("<text...>", "Message text")
1134
+ .requiredOption("--to <name>", "Target running local Akemon name")
1135
+ .option("--from <name>", "Source Akemon name (defaults to configured local manager)")
1136
+ .option("-p, --port <port>", "Target local Akemon port (overrides --to lookup)")
1137
+ .option("--conversation <id>", "Conversation id for this local interconnect message")
1138
+ .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "task")
1139
+ .option("--require-owner-approval", "Ask the target Akemon owner to approve/respond before engine execution")
1140
+ .option("--json", "Print raw JSON")
1141
+ .action(async (textParts, opts) => {
1142
+ const text = textParts.join(" ").trim();
1143
+ if (!text) {
1144
+ console.error("Message text is required");
1145
+ process.exit(1);
1146
+ }
1147
+ const from = opts.from || await getLocalManagerName();
1148
+ if (!from) {
1149
+ console.error("Missing --from. Set a default manager with: akemon manager <name>");
1150
+ process.exit(1);
1151
+ }
1152
+ const message = createLocalAkemonMessage({
1153
+ sourceAgent: from,
1154
+ targetAgent: opts.to,
1155
+ text,
1156
+ conversationId: opts.conversation,
1157
+ memoryScope: parseAkemonMemoryScope(opts.memoryScope),
1158
+ requiresOwnerApproval: opts.requireOwnerApproval === true,
1159
+ });
1160
+ const data = await callLocalOwnerEndpoint("/self/message", { name: opts.to, port: opts.port }, {
1161
+ method: "POST",
1162
+ body: JSON.stringify({ message }),
1163
+ });
1164
+ await appendLocalPeerContact({
1165
+ schemaVersion: 1,
1166
+ ts: new Date().toISOString(),
1167
+ ownerAgent: from,
1168
+ peerAgent: opts.to,
1169
+ direction: "sent",
1170
+ messageId: message.id,
1171
+ conversationId: message.conversationId,
1172
+ });
1173
+ if (opts.json) {
1174
+ console.log(JSON.stringify(data, null, 2));
1175
+ return;
1176
+ }
1177
+ console.log(data.output || data.response?.payload?.text || "");
1178
+ if (data.response?.id)
1179
+ console.error(`[akemon-message] response: ${data.response.id}`);
1180
+ });
1181
+ const chatCommand = addLocalAkemonSelectionOptions(program
1182
+ .command("chat")
1183
+ .description("Send an owner chat intent through the local Secretariat Runtime")
1184
+ .argument("[text...]", "Message text"))
1185
+ .option("--conversation <id>", "Conversation id for this chat", "akemon_cli")
1186
+ .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
1187
+ .option("--no-wait", "Submit and return after the local task is accepted")
1188
+ .option("--json", "Print raw JSON")
1189
+ .addOption(new Option("--message-id <id>", "Internal: fixed message id for detached submission").hideHelp())
1190
+ .addOption(new Option("--created-at <iso>", "Internal: fixed createdAt for detached submission").hideHelp())
1191
+ .action(async (textParts, opts) => {
1192
+ await runChatSendCli(textParts || [], opts);
1193
+ });
1194
+ addLocalAkemonSelectionOptions(chatCommand
1195
+ .command("follow")
1196
+ .description("Queue a follow-up for a running Akemon task")
1197
+ .argument("<taskId>", "Task id")
1198
+ .argument("<text...>", "Follow-up text"))
1199
+ .option("--conversation <id>", "Conversation id for this follow-up; defaults to the task conversation")
1200
+ .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
1201
+ .option("--json", "Print raw JSON")
1202
+ .action(async (taskId, textParts, opts) => {
1203
+ await runChatFollowCli(taskId, textParts || [], opts);
1204
+ });
1205
+ const taskCommand = addLocalAkemonSelectionOptions(program
1206
+ .command("task")
1207
+ .description("Inspect Secretariat task lifecycle records"));
1208
+ addLocalAkemonSelectionOptions(taskCommand
1209
+ .command("status")
1210
+ .description("Show a Secretariat task status")
1211
+ .argument("<taskId>", "Task id"))
1212
+ .option("--json", "Print raw JSON")
1213
+ .action(async (taskId, opts) => {
1214
+ await runTaskStatusCli(taskId, opts);
1215
+ });
1216
+ addLocalAkemonSelectionOptions(taskCommand
1217
+ .command("report")
1218
+ .description("Show a Secretariat task report")
1219
+ .argument("<taskId>", "Task id"))
1220
+ .option("--json", "Print raw JSON")
1221
+ .action(async (taskId, opts) => {
1222
+ await runTaskReportCli(taskId, opts);
1223
+ });
1224
+ addLocalAkemonSelectionOptions(taskCommand
1225
+ .command("watch")
1226
+ .description("Poll a Secretariat task until it completes")
1227
+ .argument("<taskId>", "Task id"))
1228
+ .option("--interval-ms <ms>", "Polling interval in milliseconds", "1000")
1229
+ .option("--jsonl", "Print status updates as JSON Lines")
1230
+ .option("--once", "Print one status update and exit")
1231
+ .action(async (taskId, opts) => {
1232
+ await runTaskWatchCli(taskId, opts);
1233
+ });
384
1234
  program
385
1235
  .command("software-agent")
386
- .description("Run an owner-only local software-agent task via a running akemon serve")
1236
+ .description("Run an owner-only software-agent task via a running local Akemon")
387
1237
  .argument("<goal...>", "Task goal to send to the software agent")
388
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1238
+ .option("-n, --name <name>", "Running local Akemon name")
1239
+ .option("-p, --port <port>", "Local Akemon port (overrides --name)")
389
1240
  .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
390
1241
  .option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
391
1242
  .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
@@ -403,10 +1254,11 @@ program
403
1254
  });
404
1255
  program
405
1256
  .command("software-agent-continue")
406
- .description("Continue an Akemon-side software-agent context session")
1257
+ .description("Continue an Akemon-side software-agent context session via a running local Akemon")
407
1258
  .argument("<sessionId>", "Akemon-side context session id to continue")
408
1259
  .argument("<goal...>", "Task goal to send to the software agent")
409
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1260
+ .option("-n, --name <name>", "Running local Akemon name")
1261
+ .option("-p, --port <port>", "Local Akemon port (overrides --name)")
410
1262
  .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
411
1263
  .option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
412
1264
  .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
@@ -421,21 +1273,19 @@ program
421
1273
  .action(async (sessionId, goalParts, opts) => {
422
1274
  await runSoftwareAgentCli(goalParts, opts, sessionId);
423
1275
  });
424
- program
1276
+ addLocalAkemonSelectionOptions(program
425
1277
  .command("software-agent-status")
426
- .description("Show the owner-only local software-agent peripheral state")
427
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1278
+ .description("Show the owner-only local software-agent peripheral state"))
428
1279
  .action(async (opts) => {
429
1280
  const data = await callLocalOwnerEndpoint("/self/software-agent/status", opts, {
430
1281
  method: "GET",
431
1282
  });
432
1283
  console.log(JSON.stringify(data, null, 2));
433
1284
  });
434
- program
1285
+ addLocalAkemonSelectionOptions(program
435
1286
  .command("software-agent-tasks")
436
1287
  .description("List recent owner-only software-agent task ledger records")
437
- .argument("[taskId]", "Task id to inspect")
438
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1288
+ .argument("[taskId]", "Task id to inspect"))
439
1289
  .option("-l, --limit <n>", "Maximum recent tasks to list", "20")
440
1290
  .option("--session <id>", "Filter listed tasks by Akemon-side context session id")
441
1291
  .option("--context", "Print the task's Akemon TASK_CONTEXT.md content when inspecting one task")
@@ -472,11 +1322,10 @@ program
472
1322
  }
473
1323
  printSoftwareAgentTaskList(Array.isArray(data.tasks) ? data.tasks : []);
474
1324
  });
475
- program
1325
+ addLocalAkemonSelectionOptions(program
476
1326
  .command("software-agent-sessions")
477
1327
  .description("List or inspect owner-only Akemon-side software-agent context sessions")
478
- .argument("[sessionId]", "Context session id to inspect")
479
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1328
+ .argument("[sessionId]", "Context session id to inspect"))
480
1329
  .option("-l, --limit <n>", "Maximum recent sessions to list", "20")
481
1330
  .option("--context", "Print the session TASK_CONTEXT.md content")
482
1331
  .option("--json", "Print raw JSON")
@@ -509,16 +1358,247 @@ program
509
1358
  }
510
1359
  printSoftwareAgentSessionList(Array.isArray(data.sessions) ? data.sessions : []);
511
1360
  });
512
- program
1361
+ addLocalAkemonSelectionOptions(program
513
1362
  .command("software-agent-reset")
514
- .description("Reset the owner-only local software-agent peripheral session")
515
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1363
+ .description("Reset the owner-only local software-agent peripheral session"))
516
1364
  .action(async (opts) => {
517
1365
  const data = await callLocalOwnerEndpoint("/self/software-agent/reset", opts, {
518
1366
  method: "POST",
519
1367
  });
520
1368
  console.log(JSON.stringify(data, null, 2));
521
1369
  });
1370
+ addLocalAkemonSelectionOptions(program
1371
+ .command("workbench")
1372
+ .description("Open the local owner-only Workbench UI"))
1373
+ .option("--no-open", "Print the Workbench URL without opening a browser")
1374
+ .option("--print-url", "Print the Workbench URL without opening a browser")
1375
+ .action(async (opts) => {
1376
+ await openWorkbenchUiCli(opts);
1377
+ });
1378
+ addLocalAkemonSelectionOptions(program
1379
+ .command("workbench-start")
1380
+ .description("Start an owner-only interactive Workbench PTY session")
1381
+ .argument("<tool>", "codex|claude|cursor|shell|custom")
1382
+ .argument("[args...]", "Arguments passed to the Workbench command")
1383
+ .allowUnknownOption(true))
1384
+ .option("--command <cmd>", "Override the command to spawn")
1385
+ .option("-w, --workdir <path>", "Workbench session workdir (default: serve workdir)")
1386
+ .option("--allow-outside-workdir", "Allow the Workbench workdir to be outside the serve workdir")
1387
+ .option("--cols <n>", "Terminal columns")
1388
+ .option("--rows <n>", "Terminal rows")
1389
+ .option("--input <text>", "Initial owner-direct input to write after startup")
1390
+ .option("--label <text>", "Human-readable session label")
1391
+ .option("--json", "Print raw JSON")
1392
+ .action(async (tool, args, opts) => {
1393
+ await runWorkbenchStartCli(tool, args || [], opts);
1394
+ });
1395
+ addLocalAkemonSelectionOptions(program
1396
+ .command("workbench-sessions")
1397
+ .description("List owner-only Workbench PTY sessions"))
1398
+ .option("--json", "Print raw JSON")
1399
+ .action(async (opts) => {
1400
+ await listWorkbenchSessionsCli(opts);
1401
+ });
1402
+ addLocalAkemonSelectionOptions(program
1403
+ .command("workbench-tail")
1404
+ .description("Print the rolling output buffer for a Workbench session")
1405
+ .argument("<sessionId>", "Workbench session id"))
1406
+ .option("--json", "Print raw JSON")
1407
+ .action(async (sessionId, opts) => {
1408
+ await showWorkbenchTailCli(sessionId, opts);
1409
+ });
1410
+ addLocalAkemonSelectionOptions(program
1411
+ .command("workbench-input")
1412
+ .description("Send owner-direct input to a running Workbench session")
1413
+ .argument("<sessionId>", "Workbench session id")
1414
+ .argument("<input...>", "Text to send"))
1415
+ .option("--no-enter", "Do not append a newline to the input")
1416
+ .option("--json", "Print raw JSON")
1417
+ .action(async (sessionId, inputParts, opts) => {
1418
+ await sendWorkbenchInputCli(sessionId, inputParts || [], opts);
1419
+ });
1420
+ addLocalAkemonSelectionOptions(program
1421
+ .command("workbench-stop")
1422
+ .description("Stop a Workbench PTY session")
1423
+ .argument("<sessionId>", "Workbench session id"))
1424
+ .option("--json", "Print raw JSON")
1425
+ .action(async (sessionId, opts) => {
1426
+ await stopWorkbenchSessionCli(sessionId, opts);
1427
+ });
1428
+ addLocalAkemonSelectionOptions(program
1429
+ .command("workbench-resize")
1430
+ .description("Resize a running Workbench PTY session")
1431
+ .argument("<sessionId>", "Workbench session id"))
1432
+ .option("--cols <n>", "Terminal columns")
1433
+ .option("--rows <n>", "Terminal rows")
1434
+ .option("--json", "Print raw JSON")
1435
+ .action(async (sessionId, opts) => {
1436
+ await resizeWorkbenchSessionCli(sessionId, opts);
1437
+ });
1438
+ const memoryRecordersCommand = program
1439
+ .command("memory-recorders")
1440
+ .description("List and inspect Akemon memory-writing situations")
1441
+ .action(() => {
1442
+ printMemoryRecorderList(listMemoryRecorders({ enabled: true }));
1443
+ });
1444
+ memoryRecordersCommand
1445
+ .command("list")
1446
+ .description("List registered built-in memory recorders")
1447
+ .option("--scope <scope>", "Filter by destination scope")
1448
+ .option("--source <source>", "Filter by source: builtin, module, or skill")
1449
+ .option("--json", "Print raw JSON")
1450
+ .action((opts) => {
1451
+ const recorders = listMemoryRecorders({
1452
+ scope: parseMemoryRecorderScope(opts.scope),
1453
+ source: parseMemoryRecorderSource(opts.source),
1454
+ enabled: true,
1455
+ });
1456
+ if (opts.json) {
1457
+ console.log(JSON.stringify(recorders, null, 2));
1458
+ return;
1459
+ }
1460
+ printMemoryRecorderList(recorders);
1461
+ });
1462
+ memoryRecordersCommand
1463
+ .command("show")
1464
+ .description("Show one memory recorder's trigger, destinations, and privacy boundary")
1465
+ .argument("<id>", "Memory recorder id")
1466
+ .option("--json", "Print raw JSON")
1467
+ .action((id, opts) => {
1468
+ const recorder = getMemoryRecorder(id);
1469
+ if (!recorder) {
1470
+ console.error(`Unknown memory recorder: ${id}`);
1471
+ process.exit(1);
1472
+ }
1473
+ if (opts.json) {
1474
+ console.log(JSON.stringify(recorder, null, 2));
1475
+ return;
1476
+ }
1477
+ printMemoryRecorderDetail(recorder);
1478
+ });
1479
+ const peripheralsCommand = program
1480
+ .command("peripherals")
1481
+ .description("List, register, and explore Akemon peripheral records")
1482
+ .action(() => {
1483
+ peripheralsCommand.help();
1484
+ });
1485
+ peripheralsCommand
1486
+ .command("list")
1487
+ .description("List configured peripheral records")
1488
+ .option("-n, --name <name>", "Akemon name (defaults to the configured local manager)")
1489
+ .option("--json", "Print raw JSON")
1490
+ .action(async (opts) => {
1491
+ try {
1492
+ const agentName = await resolvePeripheralRegistryAgentName(opts.name);
1493
+ const records = await loadPeripheralRecords(agentName);
1494
+ if (opts.json) {
1495
+ console.log(JSON.stringify(records, null, 2));
1496
+ return;
1497
+ }
1498
+ printPeripheralRecords(records);
1499
+ }
1500
+ catch (error) {
1501
+ console.error(error instanceof Error ? error.message : String(error));
1502
+ process.exit(1);
1503
+ }
1504
+ });
1505
+ peripheralsCommand
1506
+ .command("register")
1507
+ .description("Register or update a configured peripheral record")
1508
+ .requiredOption("--id <id>", "Stable peripheral id")
1509
+ .requiredOption("--label <name>", "Human-readable peripheral name")
1510
+ .option("-n, --name <name>", "Akemon name (defaults to the configured local manager)")
1511
+ .option("--type <type>", "Peripheral type: engine|relay|software-agent|mcp|service|hardware|custom", "custom")
1512
+ .option("--capabilities <items>", "Comma-separated capabilities")
1513
+ .option("--tags <items>", "Comma-separated tags")
1514
+ .option("--risk <level>", "Risk level: low|medium|high", "medium")
1515
+ .option("--allowed-actions <items>", "Comma-separated allowed action labels")
1516
+ .option("--command <command>", "Optional start command, recorded but not executed")
1517
+ .option("--url <url>", "Optional URL for this peripheral")
1518
+ .option("--explore <mode>", "Explore mode: none|plain-text", "none")
1519
+ .option("--explore-description <text>", "Plain-text explore behavior description")
1520
+ .option("--json", "Print raw JSON")
1521
+ .action(async (opts) => {
1522
+ try {
1523
+ const agentName = await resolvePeripheralRegistryAgentName(opts.name);
1524
+ const record = await upsertPeripheralRecord(agentName, {
1525
+ id: opts.id,
1526
+ name: opts.label,
1527
+ type: parsePeripheralRecordType(opts.type),
1528
+ capabilities: parseCommaSeparatedCliOption(opts.capabilities) || [],
1529
+ tags: parseCommaSeparatedCliOption(opts.tags) || [],
1530
+ riskLevel: parsePeripheralRiskLevel(opts.risk),
1531
+ allowedActions: parseCommaSeparatedCliOption(opts.allowedActions) || [],
1532
+ startCommand: opts.command,
1533
+ url: opts.url,
1534
+ explore: {
1535
+ mode: parsePeripheralExploreMode(opts.explore),
1536
+ description: opts.exploreDescription,
1537
+ },
1538
+ source: "owner",
1539
+ status: "configured",
1540
+ updatedAt: new Date().toISOString(),
1541
+ });
1542
+ if (opts.json) {
1543
+ console.log(JSON.stringify(record, null, 2));
1544
+ return;
1545
+ }
1546
+ console.log(`Registered peripheral ${record.id} for ${agentName}.`);
1547
+ }
1548
+ catch (error) {
1549
+ console.error(error instanceof Error ? error.message : String(error));
1550
+ process.exit(1);
1551
+ }
1552
+ });
1553
+ peripheralsCommand
1554
+ .command("explore")
1555
+ .description("Print a plain-text peripheral environment briefing")
1556
+ .argument("[id]", "Optional peripheral id")
1557
+ .option("-n, --name <name>", "Akemon name (defaults to the configured local manager)")
1558
+ .option("-p, --port <port>", "Running local Akemon port for --live")
1559
+ .option("--live", "Ask the running local Akemon for live peripheral exploration")
1560
+ .option("--json", "Print raw JSON")
1561
+ .action(async (id, opts) => {
1562
+ try {
1563
+ let briefing;
1564
+ if (opts.live) {
1565
+ const query = id ? `?id=${encodeURIComponent(id)}` : "";
1566
+ briefing = await callLocalOwnerEndpoint(`/self/peripherals/explore${query}`, opts, { method: "GET" });
1567
+ }
1568
+ else {
1569
+ const agentName = await resolvePeripheralRegistryAgentName(opts.name);
1570
+ const records = await loadPeripheralRecords(agentName);
1571
+ briefing = await buildPeripheralExploreBriefing({ records, id });
1572
+ }
1573
+ if (opts.json) {
1574
+ console.log(JSON.stringify(briefing, null, 2));
1575
+ return;
1576
+ }
1577
+ process.stdout.write(briefing.text);
1578
+ if (!briefing.text.endsWith("\n"))
1579
+ process.stdout.write("\n");
1580
+ }
1581
+ catch (error) {
1582
+ console.error(error instanceof Error ? error.message : String(error));
1583
+ process.exit(1);
1584
+ }
1585
+ });
1586
+ program
1587
+ .command("audit")
1588
+ .argument("[command]", "Optional subcommand: list")
1589
+ .description("List Akemon permission and action audit records")
1590
+ .option("-n, --name <name>", "Agent name (defaults to the configured local manager)")
1591
+ .option("-l, --limit <n>", "Maximum records to list", "20")
1592
+ .option("--kind <kind>", "Filter by kind: software-agent-task|akemon-message|relay-publication|memory-write")
1593
+ .option("--decision <decision>", "Filter by decision: allowed|denied")
1594
+ .option("--json", "Print raw JSON")
1595
+ .action(async (command, opts) => {
1596
+ if (command !== undefined && command !== "list") {
1597
+ console.error("Unknown audit command. Use: akemon audit [list]");
1598
+ process.exit(1);
1599
+ }
1600
+ await runPermissionAuditListCli(opts);
1601
+ });
522
1602
  program
523
1603
  .command("privacy-filter")
524
1604
  .description("Sanitize text with built-in redaction and optional OpenAI Privacy Filter")
@@ -564,6 +1644,7 @@ program
564
1644
  .description("Print a work-memory context packet for external software agents")
565
1645
  .option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
566
1646
  .option("-n, --name <name>", "Agent name", "my-agent")
1647
+ .option("--global", "Use global Akemon work memory under the agent home")
567
1648
  .option("--purpose <text>", "Purpose of this context packet", "external software-agent work context")
568
1649
  .option("--budget <chars>", "Maximum packet size in characters", "12000")
569
1650
  .option("--json", "Print raw JSON")
@@ -574,6 +1655,7 @@ program
574
1655
  agentName: opts.name,
575
1656
  purpose: opts.purpose,
576
1657
  budget: parsePositiveIntCliOption(opts.budget, "--budget"),
1658
+ global: opts.global === true,
577
1659
  });
578
1660
  if (opts.json) {
579
1661
  console.log(JSON.stringify(packet, null, 2));
@@ -594,6 +1676,7 @@ program
594
1676
  .argument("<text...>", "Durable work-memory note")
595
1677
  .option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
596
1678
  .option("-n, --name <name>", "Agent name", "my-agent")
1679
+ .option("--global", "Use global Akemon work memory under the agent home")
597
1680
  .option("--source <source>", "Note source, e.g. user, codex, or claude-code", "user")
598
1681
  .option("--session <id>", "External or Akemon-side session id")
599
1682
  .option("--kind <kind>", "Work-memory kind, e.g. note, decision, command, project", "note")
@@ -609,6 +1692,7 @@ program
609
1692
  sessionId: opts.session,
610
1693
  kind: opts.kind,
611
1694
  target: opts.target,
1695
+ global: opts.global === true,
612
1696
  });
613
1697
  if (opts.json) {
614
1698
  console.log(JSON.stringify(result, null, 2));
@@ -627,10 +1711,14 @@ program
627
1711
  .description("Open your agent dashboard in the browser")
628
1712
  .action(async () => {
629
1713
  const credentials = await getOrCreateRelayCredentials();
630
- const url = `${RELAY_HTTP}/owner?account=${credentials.accountId}`;
1714
+ const url = `${DEFAULT_RELAY_HTTP}/owner?account=${credentials.accountId}`;
631
1715
  console.log(`Opening dashboard: ${url}`);
632
- const { exec } = await import("child_process");
633
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
634
- exec(`${cmd} "${url}"`);
1716
+ try {
1717
+ await openUrl(url);
1718
+ }
1719
+ catch (error) {
1720
+ console.error(error instanceof Error ? error.message : String(error));
1721
+ process.exit(1);
1722
+ }
635
1723
  });
636
1724
  program.parse();