akemon 0.3.5 → 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 (54) hide show
  1. package/DATA_POLICY.md +128 -0
  2. package/README.md +156 -19
  3. package/TRADEMARK.md +74 -0
  4. package/dist/akemon-home.js +56 -0
  5. package/dist/akemon-message.js +107 -0
  6. package/dist/best-effort.js +8 -0
  7. package/dist/cli.js +1411 -132
  8. package/dist/cognitive-artifact-store.js +101 -0
  9. package/dist/cognitive-event-log.js +47 -0
  10. package/dist/config.js +45 -9
  11. package/dist/context.js +27 -6
  12. package/dist/core/contracts/layers.js +1 -0
  13. package/dist/core/contracts/permission.js +1 -0
  14. package/dist/core/contracts/workspace.js +1 -0
  15. package/dist/core-cognitive-module.js +768 -0
  16. package/dist/engine-peripheral.js +127 -26
  17. package/dist/engine-routing.js +58 -17
  18. package/dist/interactive-session.js +361 -0
  19. package/dist/local-interconnect.js +156 -0
  20. package/dist/local-registry.js +178 -0
  21. package/dist/mcp-server.js +4 -1
  22. package/dist/memory-proposal.js +379 -0
  23. package/dist/memory-recorder.js +368 -0
  24. package/dist/orphan-scan.js +36 -24
  25. package/dist/passive-reflection-cognitive-module.js +172 -0
  26. package/dist/peripheral-registry.js +235 -0
  27. package/dist/permission-audit.js +132 -0
  28. package/dist/relay-client.js +68 -9
  29. package/dist/relay-mode.js +34 -0
  30. package/dist/relay-peripheral.js +139 -49
  31. package/dist/runtime-platform.js +122 -0
  32. package/dist/secretariat/client.js +87 -0
  33. package/dist/self.js +15 -6
  34. package/dist/server.js +3695 -439
  35. package/dist/social-discovery.js +231 -0
  36. package/dist/software-agent-peripheral.js +314 -235
  37. package/dist/software-agent-result-cli.js +69 -0
  38. package/dist/software-agent-stream-cli.js +23 -0
  39. package/dist/software-agent-transport.js +177 -0
  40. package/dist/task-module.js +243 -0
  41. package/dist/task-registry.js +756 -0
  42. package/dist/vendor/xterm/addon-fit.js +2 -0
  43. package/dist/vendor/xterm/addon-search.js +2 -0
  44. package/dist/vendor/xterm/addon-web-links.js +2 -0
  45. package/dist/vendor/xterm/xterm.css +285 -0
  46. package/dist/vendor/xterm/xterm.js +2 -0
  47. package/dist/work-memory.js +339 -0
  48. package/dist/workbench-peripheral-guide.js +79 -0
  49. package/dist/workbench-session.js +1074 -0
  50. package/dist/workbench.html +4011 -0
  51. package/package.json +11 -4
  52. package/scripts/build.cjs +24 -0
  53. package/scripts/check-architecture-baseline.cjs +68 -0
  54. package/scripts/test.cjs +38 -0
package/dist/cli.js CHANGED
@@ -1,21 +1,31 @@
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";
9
9
  import { PrivacyFilterUnavailableError, sanitizeText, } from "./privacy-filter.js";
10
10
  import { SoftwareAgentStreamCliRenderer } from "./software-agent-stream-cli.js";
11
+ import { appendWorkMemoryNote, buildWorkMemoryContext, } from "./work-memory.js";
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";
11
22
  import { readFileSync } from "fs";
12
23
  import { fileURLToPath } from "url";
13
24
  import { dirname, join } from "path";
14
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
26
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
16
- const RELAY_WS = "wss://relay.akemon.dev";
17
- const RELAY_HTTP = "https://relay.akemon.dev";
18
27
  const program = new Command();
28
+ const ALL_MODULES = ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
19
29
  function parsePortOption(port) {
20
30
  const value = typeof port === "number" ? port : parseInt(String(port || "3000"));
21
31
  return Number.isInteger(value) && value > 0 ? value : 3000;
@@ -47,6 +57,57 @@ function parseSoftwareAgentEnvPolicy(value) {
47
57
  console.error("--software-agent-env must be one of: inherit, allowlist");
48
58
  process.exit(1);
49
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
+ }
50
111
  function parseCommaSeparatedCliOption(value) {
51
112
  if (!value)
52
113
  return undefined;
@@ -76,9 +137,254 @@ function printSoftwareAgentTaskList(tasks) {
76
137
  : "no-git";
77
138
  const goal = truncateOneLine(task.envelope?.goal || "", 90);
78
139
  console.log(`${task.taskId} ${task.status}/${result} ${duration} ${git} ${task.updatedAt || task.startedAt}`);
140
+ if (task.contextSession?.sessionId)
141
+ console.log(` session: ${task.contextSession.sessionId}`);
142
+ const workMemoryDir = task.envelope?.workMemoryDir || task.result?.workMemoryDir;
143
+ if (workMemoryDir)
144
+ console.log(` work memory: ${workMemoryDir}`);
145
+ if (goal)
146
+ console.log(` ${goal}`);
147
+ }
148
+ }
149
+ function printSoftwareAgentSessionList(sessions) {
150
+ if (!sessions.length) {
151
+ console.log("No software-agent context sessions found.");
152
+ return;
153
+ }
154
+ for (const session of sessions) {
155
+ const result = session.lastResult?.success === true ? "ok" : session.lastResult?.success === false ? "error" : "pending";
156
+ const duration = typeof session.lastResult?.durationMs === "number" ? `${session.lastResult.durationMs}ms` : "-";
157
+ const updatedAt = session.updatedAt || "-";
158
+ const goal = truncateOneLine(session.lastGoal || "", 90);
159
+ console.log(`${session.sessionId} ${result} ${duration} ${updatedAt}`);
160
+ if (session.lastTaskId)
161
+ console.log(` last task: ${session.lastTaskId}`);
79
162
  if (goal)
80
163
  console.log(` ${goal}`);
164
+ if (session.packetPath)
165
+ console.log(` context: ${session.packetPath}`);
166
+ if (session.workMemoryDir)
167
+ console.log(` work memory: ${session.workMemoryDir}`);
168
+ }
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);
81
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;
82
388
  }
83
389
  function truncateOneLine(value, max) {
84
390
  const oneLine = value.replace(/\s+/g, " ").trim();
@@ -86,6 +392,23 @@ function truncateOneLine(value, max) {
86
392
  return oneLine;
87
393
  return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
88
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
+ }
89
412
  async function callLocalOwnerEndpoint(path, opts, init = {}) {
90
413
  const res = await fetchLocalOwnerEndpoint(path, opts, init);
91
414
  const text = await res.text();
@@ -103,10 +426,10 @@ async function callLocalOwnerEndpoint(path, opts, init = {}) {
103
426
  return data;
104
427
  }
105
428
  async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
106
- const credentials = await getOrCreateRelayCredentials();
107
- const port = parsePortOption(opts.port);
429
+ const secretKey = await getOrCreateLocalOwnerSecret();
430
+ const port = await resolveLocalOwnerPort(opts);
108
431
  const headers = {
109
- Authorization: `Bearer ${credentials.secretKey}`,
432
+ Authorization: `Bearer ${secretKey}`,
110
433
  };
111
434
  if (init.body !== undefined)
112
435
  headers["Content-Type"] = "application/json";
@@ -127,13 +450,51 @@ async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
127
450
  process.exit(1);
128
451
  }
129
452
  if (error instanceof TypeError && error.message === "fetch failed") {
130
- 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}`);
131
454
  process.exit(1);
132
455
  }
133
456
  throw error;
134
457
  }
135
458
  return res;
136
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
+ }
137
498
  async function streamLocalOwnerEndpoint(path, opts, body) {
138
499
  const res = await fetchLocalOwnerEndpoint(path, opts, {
139
500
  method: "POST",
@@ -176,55 +537,425 @@ async function streamLocalOwnerEndpoint(path, opts, body) {
176
537
  if (failed)
177
538
  process.exit(1);
178
539
  }
179
- program
180
- .name("akemon")
181
- .description("Agent work marketplace — train your agent, let it work for others")
182
- .version(pkg.version);
183
- program
184
- .command("serve")
185
- .description("Publish your agent to the akemon relay")
186
- .option("-p, --port <port>", "Local port for MCP loopback", "3000")
187
- .option("-w, --workdir <path>", "Working directory for the engine (default: cwd)")
188
- .option("-n, --name <name>", "Agent name", "my-agent")
189
- .option("-m, --model <model>", "Model to use (e.g. claude-sonnet-4-6, gpt-4o)")
190
- .option("--engine <engine>", "Engine: claude, codex, opencode, gemini, raw, human, or any CLI", "claude")
191
- .option("--desc <description>", "Agent description (for discovery)")
192
- .option("--tags <tags>", "Comma-separated tags (e.g. vue,frontend,review)")
193
- .option("--public", "Allow anyone to call this agent without a key")
194
- .option("--max-tasks <n>", "Maximum tasks per day (PP)")
195
- .option("--approve", "Review every task before execution")
196
- .option("--mock", "Use mock responses (for demo/testing)")
197
- .option("--allow-all", "Skip all permission prompts (for self-use)")
198
- .option("--price <n>", "Price in credits per call (default: 1)", "1")
199
- .option("--mcp-server <command>", "Wrap a community MCP server (stdio) and expose its tools via relay")
200
- .option("--avatar <url>", "Custom avatar URL (default: auto-generated from name)")
201
- .option("--notify <url>", "ntfy.sh topic URL for push notifications (e.g. https://ntfy.sh/my-agent)")
202
- .option("--interval <minutes>", "Consciousness cycle interval in minutes (default: 1440 = 24h)")
203
- .option("--with <modules>", "Enable specific modules (comma-separated: biostate,memory)")
204
- .option("--without <modules>", "Disable specific modules (comma-separated: biostate,memory)")
205
- .option("--script <name>", "Script to load for ScriptModule (default: daily-life)", "daily-life")
206
- .option("--terminal", "Enable remote terminal access (PTY)")
207
- .option("--software-agent-env <policy>", "Software-agent child environment policy: inherit or allowlist", process.env.AKEMON_SOFTWARE_AGENT_ENV_POLICY || "inherit")
208
- .option("--software-agent-env-allow <vars>", "Comma-separated extra env vars for software-agent allowlist")
209
- .option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
210
- .action(async (opts) => {
211
- const port = parseInt(opts.port);
212
- const engine = opts.engine || "claude";
213
- // Connect to relay
214
- const credentials = await getOrCreateRelayCredentials();
215
- // Derive relay HTTP URL from WS URL
216
- const relayWs = opts.relay;
217
- const relayHttp = relayWs.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
218
- // Parse module selection
219
- const ALL_MODULES = ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
220
- let enabledModules;
540
+ async function runSoftwareAgentCli(goalParts, opts, forcedSessionId) {
541
+ const body = {
542
+ goal: goalParts.join(" "),
543
+ roleScope: opts.roleScope,
544
+ memoryScope: opts.memoryScope,
545
+ riskLevel: opts.risk,
546
+ };
547
+ if (opts.workdir)
548
+ body.workdir = opts.workdir;
549
+ if (opts.allowOutsideWorkdir)
550
+ body.allowOutsideWorkdir = true;
551
+ if (opts.memorySummary)
552
+ body.memorySummary = opts.memorySummary;
553
+ const workContextBudget = parsePositiveIntCliOption(opts.workContextBudget, "--work-context-budget");
554
+ if (opts.workContext || workContextBudget !== undefined)
555
+ body.includeWorkMemoryContext = true;
556
+ if (workContextBudget !== undefined)
557
+ body.workMemoryContextBudget = workContextBudget;
558
+ const sessionId = forcedSessionId || opts.session;
559
+ if (sessionId)
560
+ body.contextSessionId = sessionId;
561
+ if (opts.deliverable)
562
+ body.deliverable = opts.deliverable;
563
+ if (opts.timeoutMs) {
564
+ const timeoutMs = Number(opts.timeoutMs);
565
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
566
+ console.error("--timeout-ms must be a positive integer");
567
+ process.exit(1);
568
+ }
569
+ body.timeoutMs = timeoutMs;
570
+ }
571
+ if (opts.stream !== false) {
572
+ await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
573
+ return;
574
+ }
575
+ const res = await fetchLocalOwnerEndpoint("/self/software-agent/run", opts, {
576
+ method: "POST",
577
+ body: JSON.stringify(body),
578
+ });
579
+ const text = await res.text();
580
+ let data;
581
+ try {
582
+ data = text ? JSON.parse(text) : {};
583
+ }
584
+ catch {
585
+ data = { output: text };
586
+ }
587
+ if (!res.ok) {
588
+ console.error(data.error || text || `Request failed with HTTP ${res.status}`);
589
+ process.exit(1);
590
+ }
591
+ const failed = renderSoftwareAgentRunResult(data);
592
+ if (failed)
593
+ process.exit(1);
594
+ }
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) {
221
931
  if (opts.with) {
222
- enabledModules = opts.with.split(",").map((m) => m.trim());
932
+ return opts.with.split(",").map((m) => m.trim()).filter(Boolean);
223
933
  }
224
- else if (opts.without) {
225
- const disabled = opts.without.split(",").map((m) => m.trim());
226
- enabledModules = ALL_MODULES.filter(m => !disabled.includes(m));
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));
937
+ }
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
+ });
950
+ }
951
+ catch (error) {
952
+ console.error(error instanceof Error ? error.message : String(error));
953
+ process.exit(1);
227
954
  }
955
+ let relayCredentials = null;
956
+ const secretKey = relayMode.enabled
957
+ ? (relayCredentials = await getOrCreateRelayCredentials()).secretKey
958
+ : await getOrCreateLocalOwnerSecret();
228
959
  serve({
229
960
  port,
230
961
  workdir: opts.workdir,
@@ -234,37 +965,65 @@ program
234
965
  approve: opts.approve,
235
966
  allowAll: opts.allowAll,
236
967
  engine,
237
- relayHttp,
238
- secretKey: credentials.secretKey,
968
+ relayHttp: relayMode.relayHttp,
969
+ secretKey,
239
970
  mcpServer: opts.mcpServer,
240
971
  cycleInterval: opts.interval ? parseInt(opts.interval) : undefined,
241
972
  notifyUrl: opts.notify,
242
- enabledModules,
973
+ enabledModules: parseEnabledModules(opts),
243
974
  scriptName: opts.script,
244
975
  softwareAgentEnvPolicy: parseSoftwareAgentEnvPolicy(opts.softwareAgentEnv),
245
976
  softwareAgentEnvAllowlist: parseCommaSeparatedCliOption(opts.softwareAgentEnvAllow),
246
977
  });
247
978
  console.log(`\nakemon v${pkg.version}`);
248
- if (!opts.public) {
249
- console.log(`Access key: ${credentials.accessKey} (share with publishers)`);
250
- }
251
- console.log(`Relay: ${relayWs}\n`);
252
- // Default avatar: DiceBear bottts-neutral (deterministic from name)
253
- const avatar = opts.avatar || `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${encodeURIComponent(opts.name)}`;
254
- connectRelay({
255
- relayUrl: relayWs,
256
- agentName: opts.name,
257
- credentials,
258
- localPort: port,
259
- description: opts.desc,
260
- isPublic: opts.public,
261
- engine,
262
- tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
263
- price: parseInt(opts.price) || 1,
264
- avatar,
265
- onOrderNotify,
266
- enableTerminal: opts.terminal,
267
- });
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);
268
1027
  });
269
1028
  program
270
1029
  .command("add")
@@ -279,7 +1038,7 @@ program
279
1038
  await addAgent(name, endpoint, opts.key, platform);
280
1039
  }
281
1040
  else {
282
- const relayEndpoint = `${RELAY_HTTP}/v1/agent/${name}/mcp`;
1041
+ const relayEndpoint = `${DEFAULT_RELAY_HTTP}/v1/agent/${name}/mcp`;
283
1042
  await addAgent(name, relayEndpoint, opts.key, platform);
284
1043
  }
285
1044
  });
@@ -288,109 +1047,558 @@ program
288
1047
  .description("List available agents on the relay")
289
1048
  .option("--search <query>", "Filter by name or description")
290
1049
  .action(async (opts) => {
291
- 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
+ }
292
1084
  });
293
1085
  program
294
1086
  .command("connect")
295
1087
  .description("Connect to the akemon network as a client (stdio MCP server for OpenClaw, Claude, etc.)")
296
- .option("--relay <url>", "Relay HTTP URL", RELAY_HTTP)
1088
+ .option("--relay <url>", "Relay HTTP URL", DEFAULT_RELAY_HTTP)
297
1089
  .option("--key <key>", "Access key for calling private agents")
298
1090
  .action(async (opts) => {
299
1091
  await connect({ relay: opts.relay, key: opts.key });
300
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
+ });
301
1234
  program
302
1235
  .command("software-agent")
303
- .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")
304
1237
  .argument("<goal...>", "Task goal to send to the software agent")
305
- .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)")
306
1240
  .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
307
1241
  .option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
308
1242
  .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
309
1243
  .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
310
1244
  .option("--risk <level>", "Risk level: low|medium|high", "medium")
311
1245
  .option("--memory-summary <text>", "Pre-filtered memory/context text to include")
1246
+ .option("--work-context", "Embed a bounded work-memory context packet in the task envelope")
1247
+ .option("--work-context-budget <chars>", "Maximum embedded work-memory context size; also enables --work-context")
312
1248
  .option("--session <id>", "Akemon-side context session id for explicit software-agent continuity")
313
1249
  .option("--deliverable <text>", "Expected output shape")
314
1250
  .option("--timeout-ms <ms>", "Task timeout in milliseconds")
315
1251
  .option("--no-stream", "Disable local streaming and wait for the final response")
316
1252
  .action(async (goalParts, opts) => {
317
- const body = {
318
- goal: goalParts.join(" "),
319
- roleScope: opts.roleScope,
320
- memoryScope: opts.memoryScope,
321
- riskLevel: opts.risk,
322
- };
323
- if (opts.workdir)
324
- body.workdir = opts.workdir;
325
- if (opts.allowOutsideWorkdir)
326
- body.allowOutsideWorkdir = true;
327
- if (opts.memorySummary)
328
- body.memorySummary = opts.memorySummary;
329
- if (opts.session)
330
- body.contextSessionId = opts.session;
331
- if (opts.deliverable)
332
- body.deliverable = opts.deliverable;
333
- if (opts.timeoutMs) {
334
- const timeoutMs = Number(opts.timeoutMs);
335
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
336
- console.error("--timeout-ms must be a positive integer");
337
- process.exit(1);
338
- }
339
- body.timeoutMs = timeoutMs;
340
- }
341
- if (opts.stream !== false) {
342
- await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
343
- return;
344
- }
345
- const data = await callLocalOwnerEndpoint("/self/software-agent/run", opts, {
346
- method: "POST",
347
- body: JSON.stringify(body),
348
- });
349
- if (data.output)
350
- console.log(data.output);
351
- else
352
- console.log(JSON.stringify(data, null, 2));
1253
+ await runSoftwareAgentCli(goalParts, opts);
353
1254
  });
354
1255
  program
1256
+ .command("software-agent-continue")
1257
+ .description("Continue an Akemon-side software-agent context session via a running local Akemon")
1258
+ .argument("<sessionId>", "Akemon-side context session id to continue")
1259
+ .argument("<goal...>", "Task goal to send to the software agent")
1260
+ .option("-n, --name <name>", "Running local Akemon name")
1261
+ .option("-p, --port <port>", "Local Akemon port (overrides --name)")
1262
+ .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
1263
+ .option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
1264
+ .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
1265
+ .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
1266
+ .option("--risk <level>", "Risk level: low|medium|high", "medium")
1267
+ .option("--memory-summary <text>", "Pre-filtered memory/context text to include")
1268
+ .option("--work-context", "Embed a bounded work-memory context packet in the task envelope")
1269
+ .option("--work-context-budget <chars>", "Maximum embedded work-memory context size; also enables --work-context")
1270
+ .option("--deliverable <text>", "Expected output shape")
1271
+ .option("--timeout-ms <ms>", "Task timeout in milliseconds")
1272
+ .option("--no-stream", "Disable local streaming and wait for the final response")
1273
+ .action(async (sessionId, goalParts, opts) => {
1274
+ await runSoftwareAgentCli(goalParts, opts, sessionId);
1275
+ });
1276
+ addLocalAkemonSelectionOptions(program
355
1277
  .command("software-agent-status")
356
- .description("Show the owner-only local software-agent peripheral state")
357
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1278
+ .description("Show the owner-only local software-agent peripheral state"))
358
1279
  .action(async (opts) => {
359
1280
  const data = await callLocalOwnerEndpoint("/self/software-agent/status", opts, {
360
1281
  method: "GET",
361
1282
  });
362
1283
  console.log(JSON.stringify(data, null, 2));
363
1284
  });
364
- program
1285
+ addLocalAkemonSelectionOptions(program
365
1286
  .command("software-agent-tasks")
366
1287
  .description("List recent owner-only software-agent task ledger records")
367
- .argument("[taskId]", "Task id to inspect")
368
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1288
+ .argument("[taskId]", "Task id to inspect"))
369
1289
  .option("-l, --limit <n>", "Maximum recent tasks to list", "20")
1290
+ .option("--session <id>", "Filter listed tasks by Akemon-side context session id")
1291
+ .option("--context", "Print the task's Akemon TASK_CONTEXT.md content when inspecting one task")
370
1292
  .option("--json", "Print raw JSON")
371
1293
  .action(async (taskId, opts) => {
1294
+ if (!taskId && opts.context) {
1295
+ console.error("--context requires a taskId");
1296
+ process.exit(1);
1297
+ }
1298
+ if (taskId && opts.session) {
1299
+ console.error("--session cannot be used when inspecting a single taskId");
1300
+ process.exit(1);
1301
+ }
372
1302
  const path = taskId
373
- ? `/self/software-agent/tasks/${encodeURIComponent(taskId)}`
374
- : `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
1303
+ ? `/self/software-agent/tasks/${encodeURIComponent(taskId)}${opts.context ? "?includeContext=1" : ""}`
1304
+ : `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}${opts.session ? `&session=${encodeURIComponent(opts.session)}` : ""}`;
375
1305
  const data = await callLocalOwnerEndpoint(path, opts, {
376
1306
  method: "GET",
377
1307
  });
1308
+ if (taskId && opts.context) {
1309
+ const contextPacket = data.contextSession?.contextPacket;
1310
+ if (typeof contextPacket === "string" && contextPacket.length > 0) {
1311
+ process.stdout.write(contextPacket);
1312
+ if (!contextPacket.endsWith("\n"))
1313
+ process.stdout.write("\n");
1314
+ return;
1315
+ }
1316
+ console.error("No TASK_CONTEXT.md content found for this task.");
1317
+ process.exit(1);
1318
+ }
378
1319
  if (opts.json || taskId) {
379
1320
  console.log(JSON.stringify(taskId ? data.task : data, null, 2));
380
1321
  return;
381
1322
  }
382
1323
  printSoftwareAgentTaskList(Array.isArray(data.tasks) ? data.tasks : []);
383
1324
  });
384
- program
1325
+ addLocalAkemonSelectionOptions(program
1326
+ .command("software-agent-sessions")
1327
+ .description("List or inspect owner-only Akemon-side software-agent context sessions")
1328
+ .argument("[sessionId]", "Context session id to inspect"))
1329
+ .option("-l, --limit <n>", "Maximum recent sessions to list", "20")
1330
+ .option("--context", "Print the session TASK_CONTEXT.md content")
1331
+ .option("--json", "Print raw JSON")
1332
+ .action(async (sessionId, opts) => {
1333
+ const query = sessionId && opts.context ? "?includeContext=1" : "";
1334
+ const path = sessionId
1335
+ ? `/self/software-agent/sessions/${encodeURIComponent(sessionId)}${query}`
1336
+ : `/self/software-agent/sessions?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
1337
+ const data = await callLocalOwnerEndpoint(path, opts, {
1338
+ method: "GET",
1339
+ });
1340
+ if (sessionId) {
1341
+ if (opts.context) {
1342
+ const contextPacket = data.session?.contextPacket;
1343
+ if (typeof contextPacket === "string" && contextPacket.length > 0) {
1344
+ process.stdout.write(contextPacket);
1345
+ if (!contextPacket.endsWith("\n"))
1346
+ process.stdout.write("\n");
1347
+ return;
1348
+ }
1349
+ console.error("No TASK_CONTEXT.md content found for this session.");
1350
+ process.exit(1);
1351
+ }
1352
+ console.log(JSON.stringify(data.session, null, 2));
1353
+ return;
1354
+ }
1355
+ if (opts.json) {
1356
+ console.log(JSON.stringify(data, null, 2));
1357
+ return;
1358
+ }
1359
+ printSoftwareAgentSessionList(Array.isArray(data.sessions) ? data.sessions : []);
1360
+ });
1361
+ addLocalAkemonSelectionOptions(program
385
1362
  .command("software-agent-reset")
386
- .description("Reset the owner-only local software-agent peripheral session")
387
- .option("-p, --port <port>", "Local akemon serve port", "3000")
1363
+ .description("Reset the owner-only local software-agent peripheral session"))
388
1364
  .action(async (opts) => {
389
1365
  const data = await callLocalOwnerEndpoint("/self/software-agent/reset", opts, {
390
1366
  method: "POST",
391
1367
  });
392
1368
  console.log(JSON.stringify(data, null, 2));
393
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
+ });
394
1602
  program
395
1603
  .command("privacy-filter")
396
1604
  .description("Sanitize text with built-in redaction and optional OpenAI Privacy Filter")
@@ -431,15 +1639,86 @@ program
431
1639
  throw error;
432
1640
  }
433
1641
  });
1642
+ program
1643
+ .command("work-context")
1644
+ .description("Print a work-memory context packet for external software agents")
1645
+ .option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
1646
+ .option("-n, --name <name>", "Agent name", "my-agent")
1647
+ .option("--global", "Use global Akemon work memory under the agent home")
1648
+ .option("--purpose <text>", "Purpose of this context packet", "external software-agent work context")
1649
+ .option("--budget <chars>", "Maximum packet size in characters", "12000")
1650
+ .option("--json", "Print raw JSON")
1651
+ .action(async (opts) => {
1652
+ try {
1653
+ const packet = await buildWorkMemoryContext({
1654
+ workdir: opts.workdir || process.cwd(),
1655
+ agentName: opts.name,
1656
+ purpose: opts.purpose,
1657
+ budget: parsePositiveIntCliOption(opts.budget, "--budget"),
1658
+ global: opts.global === true,
1659
+ });
1660
+ if (opts.json) {
1661
+ console.log(JSON.stringify(packet, null, 2));
1662
+ return;
1663
+ }
1664
+ process.stdout.write(packet.text);
1665
+ if (!packet.text.endsWith("\n"))
1666
+ process.stdout.write("\n");
1667
+ }
1668
+ catch (error) {
1669
+ console.error(error instanceof Error ? error.message : String(error));
1670
+ process.exit(1);
1671
+ }
1672
+ });
1673
+ program
1674
+ .command("work-note")
1675
+ .description("Append a note to Akemon work memory")
1676
+ .argument("<text...>", "Durable work-memory note")
1677
+ .option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
1678
+ .option("-n, --name <name>", "Agent name", "my-agent")
1679
+ .option("--global", "Use global Akemon work memory under the agent home")
1680
+ .option("--source <source>", "Note source, e.g. user, codex, or claude-code", "user")
1681
+ .option("--session <id>", "External or Akemon-side session id")
1682
+ .option("--kind <kind>", "Work-memory kind, e.g. note, decision, command, project", "note")
1683
+ .option("--target <path>", "Optional target file under the work memory directory")
1684
+ .option("--json", "Print raw JSON")
1685
+ .action(async (textParts, opts) => {
1686
+ try {
1687
+ const result = await appendWorkMemoryNote({
1688
+ workdir: opts.workdir || process.cwd(),
1689
+ agentName: opts.name,
1690
+ text: textParts.join(" "),
1691
+ source: opts.source,
1692
+ sessionId: opts.session,
1693
+ kind: opts.kind,
1694
+ target: opts.target,
1695
+ global: opts.global === true,
1696
+ });
1697
+ if (opts.json) {
1698
+ console.log(JSON.stringify(result, null, 2));
1699
+ return;
1700
+ }
1701
+ console.log(`Work memory note appended: ${result.note.id}`);
1702
+ console.log(`Path: ${result.path}`);
1703
+ }
1704
+ catch (error) {
1705
+ console.error(error instanceof Error ? error.message : String(error));
1706
+ process.exit(1);
1707
+ }
1708
+ });
434
1709
  program
435
1710
  .command("dashboard")
436
1711
  .description("Open your agent dashboard in the browser")
437
1712
  .action(async () => {
438
1713
  const credentials = await getOrCreateRelayCredentials();
439
- const url = `${RELAY_HTTP}/owner?account=${credentials.accountId}`;
1714
+ const url = `${DEFAULT_RELAY_HTTP}/owner?account=${credentials.accountId}`;
440
1715
  console.log(`Opening dashboard: ${url}`);
441
- const { exec } = await import("child_process");
442
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
443
- 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
+ }
444
1723
  });
445
1724
  program.parse();