@cydm/happy-elves 0.1.0-beta.82 → 0.1.0-beta.83

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 (73) hide show
  1. package/apps/cli/dist/commands/daemon.js +5 -1
  2. package/apps/cli/dist/commands/gateway.js +96 -38
  3. package/apps/cli/dist/commands/lib/args.js +17 -0
  4. package/apps/cli/dist/commands/lib/doctor.js +133 -11
  5. package/apps/cli/dist/commands/lib/json.d.ts +1 -1
  6. package/apps/cli/dist/commands/lib/json.js +5 -2
  7. package/apps/cli/dist/commands/lib/status.js +5 -1
  8. package/apps/cli/dist/commands/lib/usage.js +30 -31
  9. package/apps/cli/dist/commands/memory.js +96 -63
  10. package/apps/cli/dist/commands/relay.js +5 -1
  11. package/apps/cli/dist/commands/schedule.js +156 -143
  12. package/apps/cli/dist/commands/turn.js +59 -23
  13. package/apps/cli/package.json +6 -0
  14. package/apps/daemon/dist/gateway/adapters.d.ts +37 -0
  15. package/apps/daemon/dist/gateway/adapters.js +2 -0
  16. package/apps/daemon/dist/gateway/feishu.d.ts +69 -0
  17. package/apps/daemon/dist/gateway/feishu.js +493 -0
  18. package/apps/daemon/dist/gateway/format.d.ts +2 -0
  19. package/apps/daemon/dist/gateway/format.js +19 -0
  20. package/apps/daemon/dist/gateway/handler.d.ts +7 -0
  21. package/apps/daemon/dist/gateway/handler.js +125 -0
  22. package/apps/daemon/dist/gateway/runtime.d.ts +33 -0
  23. package/apps/daemon/dist/gateway/runtime.js +738 -0
  24. package/apps/daemon/dist/gateway/store.d.ts +91 -0
  25. package/apps/daemon/dist/gateway/store.js +125 -0
  26. package/apps/daemon/dist/memory/handler.d.ts +7 -0
  27. package/apps/daemon/dist/memory/handler.js +95 -0
  28. package/apps/daemon/dist/memory/manager.d.ts +41 -0
  29. package/apps/daemon/dist/memory/manager.js +319 -0
  30. package/apps/daemon/dist/paths.d.ts +3 -0
  31. package/apps/daemon/dist/paths.js +3 -0
  32. package/apps/daemon/dist/relay/admission.d.ts +28 -0
  33. package/apps/daemon/dist/relay/admission.js +31 -0
  34. package/apps/daemon/dist/relay/connection.js +27 -0
  35. package/apps/daemon/dist/schedule/handler.d.ts +7 -0
  36. package/apps/daemon/dist/schedule/handler.js +127 -0
  37. package/apps/daemon/dist/schedule/runner.d.ts +12 -0
  38. package/apps/daemon/dist/schedule/runner.js +447 -0
  39. package/apps/daemon/dist/schedule/store.d.ts +10 -0
  40. package/apps/daemon/dist/schedule/store.js +137 -0
  41. package/apps/daemon/dist/session/events.js +27 -29
  42. package/apps/daemon/dist/session/prompt.js +104 -2
  43. package/apps/daemon/dist/start.js +5 -1
  44. package/apps/daemon/package.json +1 -1
  45. package/apps/relay/dist/controller-handlers.d.ts +21 -0
  46. package/apps/relay/dist/controller-handlers.js +100 -2
  47. package/apps/relay/dist/db.js +73 -0
  48. package/apps/relay/dist/errors.d.ts +2 -1
  49. package/apps/relay/dist/errors.js +3 -1
  50. package/apps/relay/dist/http-routes.js +208 -5
  51. package/apps/relay/dist/http-schemas.d.ts +77 -0
  52. package/apps/relay/dist/http-schemas.js +26 -0
  53. package/apps/relay/dist/index.js +1 -1
  54. package/apps/relay/dist/machine-handlers.d.ts +12 -0
  55. package/apps/relay/dist/machine-handlers.js +57 -2
  56. package/apps/relay/dist/relay-context.d.ts +26 -1
  57. package/apps/relay/dist/relay-context.js +54 -0
  58. package/apps/relay/dist/types.d.ts +15 -0
  59. package/package.json +3 -2
  60. package/packages/agent-sdk/dist/index.d.ts +10 -0
  61. package/packages/client/dist/client.d.ts +4 -1
  62. package/packages/client/dist/client.js +62 -1
  63. package/packages/client/dist/index.d.ts +1 -1
  64. package/packages/client/dist/transport.d.ts +15 -0
  65. package/packages/client/dist/transport.js +21 -0
  66. package/packages/client/dist/types.d.ts +42 -1
  67. package/packages/runtime/dist/index.d.ts +10 -0
  68. package/packages/runtime-cli/dist/codex-app-server.js +57 -0
  69. package/packages/shared/dist/protocol-schemas.d.ts +959 -0
  70. package/packages/shared/dist/protocol-schemas.js +224 -0
  71. package/packages/shared/dist/protocol-types.d.ts +291 -0
  72. package/packages/shared/dist/protocol.d.ts +108 -2
  73. package/packages/shared/dist/protocol.js +106 -1
@@ -57,9 +57,13 @@ export async function handleDaemon({ domain, action, flags }) {
57
57
  for (const check of checks) {
58
58
  console.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`);
59
59
  }
60
+ if (!data.ok)
61
+ process.exitCode = 1;
60
62
  return true;
61
63
  }
62
- ok("daemon.doctor", data);
64
+ if (!data.ok)
65
+ process.exitCode = 1;
66
+ ok("daemon.doctor", data, {}, data.ok);
63
67
  return true;
64
68
  }
65
69
  if (domain === "daemon" && action === "config") {
@@ -1,71 +1,98 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { encryptJson } from "../../../../packages/shared/dist/index.js";
4
- import { CliError, ControllerClient, ok, readConfig, readGatewayStore, redactedGatewayChannel, requireString, updateGatewayStore, wantsJson, } from "./lib/index.js";
4
+ import { CliError, ControllerClient, ok, readConfig, readGatewayStore, requireString, updateGatewayStore, wantsJson, } from "./lib/index.js";
5
5
  export async function handleGateway({ domain, action, positional, flags }) {
6
6
  if (domain !== "gateway")
7
7
  return false;
8
8
  if (action === "enable") {
9
+ const config = await readConfig(flags);
10
+ const client = new ControllerClient(config);
9
11
  const type = gatewayChannelType(typeof flags.channel === "string" ? flags.channel : "fake");
10
12
  const id = gatewayChannelId(flags, positional[0], type);
11
13
  const targetMachineId = requireString(flags, "machine");
12
- const targetCwd = requireString(flags, "cwd");
14
+ const targetCwd = typeof flags.cwd === "string" ? flags.cwd : undefined;
15
+ const targetSessionId = typeof flags.session === "string" ? flags.session : undefined;
16
+ if (!targetCwd && !targetSessionId)
17
+ throw new CliError("Gateway enable requires --cwd or --session", "MISSING_ARGUMENT");
13
18
  const defaultAgent = typeof flags.agent === "string" ? flags.agent : "codex";
14
19
  const defaultModel = typeof flags.model === "string" ? flags.model : undefined;
15
- const localToken = typeof flags.token === "string" ? flags.token : undefined;
16
- const localSecret = typeof flags.secret === "string" ? flags.secret : undefined;
17
- if (type === "lark" && (!localToken || !localSecret)) {
18
- throw new CliError("Lark gateway requires --token and --secret; they are stored only in the local gateway config.", "MISSING_ARGUMENT");
20
+ const appId = typeof flags["app-id"] === "string" ? flags["app-id"] : undefined;
21
+ const appSecret = flags["app-secret-stdin"] === true ? await readSecretFromStdin() : undefined;
22
+ const domainValue = typeof flags.domain === "string" ? flags.domain : "feishu";
23
+ if (domainValue !== "feishu" && domainValue !== "lark")
24
+ throw new CliError("Gateway --domain must be feishu or lark", "INVALID_ARGUMENT");
25
+ const inboundMode = typeof flags.inbound === "string" ? flags.inbound : "mention";
26
+ if (inboundMode !== "mention" && inboundMode !== "all")
27
+ throw new CliError("Gateway --inbound must be mention or all", "INVALID_ARGUMENT");
28
+ const reactionEmoji = typeof flags["reaction-emoji"] === "string" ? flags["reaction-emoji"] : "Typing";
29
+ const allowedChatIds = csvFlag(flags["allow-chat"]);
30
+ const allowedSenderIds = csvFlag(flags["allow-sender"]);
31
+ const allowAnySender = flags["allow-any-sender"] === true;
32
+ if (type === "lark" && (!appId || !appSecret)) {
33
+ throw new CliError("Lark gateway requires --app-id and --app-secret-stdin; appSecret is stored only in the target daemon config.", "MISSING_ARGUMENT");
19
34
  }
20
- const now = new Date().toISOString();
21
- const store = await updateGatewayStore((current) => {
22
- const existing = current.channels.find((item) => item.id === id);
23
- const next = {
24
- ...(existing ?? { createdAt: now }),
35
+ if (type === "lark" && !allowAnySender && allowedChatIds.length === 0 && allowedSenderIds.length === 0) {
36
+ throw new CliError("Lark gateway requires --allow-any-sender or at least one --allow-chat/--allow-sender policy.", "MISSING_ARGUMENT");
37
+ }
38
+ if (type === "lark" && (typeof flags.token === "string" || typeof flags.secret === "string")) {
39
+ throw new CliError("Lark gateway no longer accepts --token/--secret; use --app-id and --app-secret-stdin.", "INVALID_ARGUMENT");
40
+ }
41
+ const result = await client.gateway({
42
+ machineId: targetMachineId,
43
+ action: "upsert",
44
+ channel: {
25
45
  id,
26
46
  type,
27
47
  enabled: true,
28
48
  targetMachineId,
49
+ targetSessionId,
29
50
  targetCwd,
30
51
  defaultAgent,
31
52
  ...(defaultModel ? { defaultModel } : {}),
32
- ...(localToken ? { localToken } : existing?.localToken ? { localToken: existing.localToken } : {}),
33
- ...(localSecret ? { localSecret } : existing?.localSecret ? { localSecret: existing.localSecret } : {}),
34
- updatedAt: now,
35
- };
36
- return {
37
- ...current,
38
- channels: [next, ...current.channels.filter((item) => item.id !== id)],
39
- };
53
+ ...(type === "lark" ? {
54
+ domain: domainValue,
55
+ appId,
56
+ appSecret,
57
+ inboundMode,
58
+ allowAnySender,
59
+ ...(allowedChatIds.length > 0 ? { allowedChatIds } : {}),
60
+ ...(allowedSenderIds.length > 0 ? { allowedSenderIds } : {}),
61
+ processingReaction: {
62
+ enabled: flags["disable-reaction"] !== true,
63
+ emojiType: reactionEmoji,
64
+ },
65
+ } : {}),
66
+ },
40
67
  });
41
- const channel = mustFindChannel(store, id);
42
- ok("gateway.enable", { channel: redactedGatewayChannel(channel) });
68
+ ok("gateway.enable", { channel: result.channel });
43
69
  return true;
44
70
  }
45
71
  if (action === "disable") {
72
+ const config = await readConfig(flags);
73
+ const client = new ControllerClient(config);
74
+ const machineId = await resolveGatewayMachineId(client, flags);
46
75
  const id = gatewayChannelId(flags, positional[0], "fake");
47
- const now = new Date().toISOString();
48
- const store = await updateGatewayStore((current) => ({
49
- ...current,
50
- channels: current.channels.map((channel) => channel.id === id ? { ...channel, enabled: false, updatedAt: now } : channel),
51
- }));
52
- const channel = mustFindChannel(store, id);
53
- ok("gateway.disable", { channel: redactedGatewayChannel(channel) });
76
+ const result = await client.gateway({ machineId, action: "disable", channelId: id });
77
+ ok("gateway.disable", { channel: result.channel });
54
78
  return true;
55
79
  }
56
80
  if (action === "status") {
57
- const store = await readGatewayStore();
58
- const data = {
59
- channels: store.channels.map(redactedGatewayChannel),
60
- mappings: store.mappings,
61
- };
81
+ const config = await readConfig(flags);
82
+ const client = new ControllerClient(config);
83
+ const machineId = await resolveGatewayMachineId(client, flags);
84
+ const result = await client.gateway({ machineId, action: "list" });
85
+ const includeDebug = flags["include-debug"] === true;
86
+ const channels = (result.channels ?? []).filter((channel) => includeDebug || channel.type === "lark");
87
+ const data = { machineId, channels };
62
88
  if (!wantsJson(flags)) {
63
89
  if (data.channels.length === 0) {
64
- console.log("No gateways configured.");
90
+ console.log(includeDebug ? "No gateways configured." : "No product gateways configured.");
65
91
  return true;
66
92
  }
67
93
  for (const channel of data.channels) {
68
- console.log(`${channel.id} ${channel.type} ${channel.enabled ? "enabled" : "disabled"} ${channel.targetMachineId} ${channel.targetCwd}`);
94
+ const target = channel.targetSessionId ? `session:${channel.targetSessionId}` : channel.targetCwd ?? "";
95
+ console.log(`${channel.id} ${channel.type} ${channel.enabled ? "enabled" : "disabled"} ${target} ${channel.listening ? "listening" : "stopped"}`);
69
96
  }
70
97
  return true;
71
98
  }
@@ -75,13 +102,19 @@ export async function handleGateway({ domain, action, positional, flags }) {
75
102
  if (action === "test") {
76
103
  const config = await readConfig(flags);
77
104
  const client = new ControllerClient(config);
105
+ const machineId = await resolveGatewayMachineId(client, flags);
78
106
  const channelId = gatewayChannelId(flags, positional[0], "fake");
79
107
  const externalConversationId = requireString(flags, "conversation");
80
108
  const externalMessageId = requireString(flags, "message-id");
81
109
  const text = requireString(flags, "text");
82
- const store = await readGatewayStore();
83
- const channel = mustFindEnabledChannel(store, channelId);
84
- const result = await deliverGatewayMessage({ store, channel, client, accountSecret: config.accountSecret, externalConversationId, externalMessageId, text });
110
+ const result = await client.gateway({
111
+ machineId,
112
+ action: "test",
113
+ channelId,
114
+ externalConversationId,
115
+ externalMessageId,
116
+ text,
117
+ });
85
118
  ok("gateway.test", result);
86
119
  return true;
87
120
  }
@@ -337,6 +370,31 @@ function gatewayChannelType(value) {
337
370
  function gatewayChannelId(flags, positional, fallbackType) {
338
371
  return positional || (typeof flags.name === "string" ? flags.name : undefined) || (typeof flags.channel === "string" ? flags.channel : fallbackType);
339
372
  }
373
+ function csvFlag(value) {
374
+ if (typeof value !== "string")
375
+ return [];
376
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
377
+ }
378
+ async function readSecretFromStdin() {
379
+ let value = "";
380
+ process.stdin.setEncoding("utf8");
381
+ for await (const chunk of process.stdin) {
382
+ value += chunk;
383
+ }
384
+ const secret = value.trim();
385
+ if (!secret)
386
+ throw new CliError("--app-secret-stdin was set but stdin was empty", "MISSING_ARGUMENT");
387
+ return secret;
388
+ }
389
+ async function resolveGatewayMachineId(client, flags) {
390
+ if (typeof flags.machine === "string" && flags.machine.length > 0)
391
+ return flags.machine;
392
+ const snapshot = await client.snapshot();
393
+ const machine = snapshot.machines.find((item) => item.online) ?? snapshot.machines[0];
394
+ if (!machine)
395
+ throw new CliError("No daemon machine is available; pass --machine <machineId>", "MACHINE_NOT_FOUND");
396
+ return machine.id;
397
+ }
340
398
  function mustFindChannel(store, id) {
341
399
  const channel = store.channels.find((item) => item.id === id);
342
400
  if (!channel)
@@ -2,22 +2,32 @@ import { parseFlags as parseSharedFlags } from "../../../../../packages/shared/d
2
2
  import { CliError } from "../../errors.js";
3
3
  const valueFlags = new Set([
4
4
  "agent",
5
+ "app-id",
5
6
  "at",
6
7
  "channel",
7
8
  "code",
8
9
  "conversation",
10
+ "cron",
9
11
  "controller",
10
12
  "cwd",
13
+ "date",
11
14
  "device-id",
12
15
  "device-name",
16
+ "every-ms",
17
+ "execution",
13
18
  "from",
14
19
  "host",
20
+ "domain",
15
21
  "invite-secret",
22
+ "inbound",
16
23
  "join-url-base",
17
24
  "label",
25
+ "layer",
18
26
  "limit",
19
27
  "machine",
20
28
  "machine-id",
29
+ "memory",
30
+ "memory-mode",
21
31
  "message-id",
22
32
  "memory-project",
23
33
  "memory-top-n",
@@ -35,6 +45,7 @@ const valueFlags = new Set([
35
45
  "prompt-file",
36
46
  "every",
37
47
  "reason",
48
+ "reaction-emoji",
38
49
  "relay",
39
50
  "root",
40
51
  "runtime-limit",
@@ -56,6 +67,7 @@ const valueFlags = new Set([
56
67
  "text-file",
57
68
  "timeout",
58
69
  "timeout-ms",
70
+ "timezone",
59
71
  "wait-timeout-ms",
60
72
  "inactivity-timeout",
61
73
  "runtime-timeout",
@@ -69,10 +81,14 @@ const valueFlags = new Set([
69
81
  ]);
70
82
  const booleanFlags = new Set([
71
83
  "all",
84
+ "allow-any-sender",
85
+ "app-secret-stdin",
72
86
  "detach",
87
+ "disable-reaction",
73
88
  "help",
74
89
  "h",
75
90
  "include-archived",
91
+ "include-debug",
76
92
  "json",
77
93
  "keep-source",
78
94
  "local",
@@ -82,6 +98,7 @@ const booleanFlags = new Set([
82
98
  "summary",
83
99
  "verbose",
84
100
  "wait",
101
+ "manual",
85
102
  ]);
86
103
  const knownFlags = new Set([...valueFlags, ...booleanFlags]);
87
104
  const dashPrefixedValueFlags = new Set(["secret"]);
@@ -1,49 +1,171 @@
1
1
  import path from "node:path";
2
+ import cliPackage from "../../../package.json" with { type: "json" };
3
+ import { ControllerClient } from "../../../../../packages/client/dist/index.js";
2
4
  import { configDir, configPath } from "./paths.js";
3
5
  import { redactedControllerConfig, redactedDaemonConfig } from "./config.js";
4
6
  import { ok, wantsJson } from "./json.js";
5
- import { localDaemonStatus } from "./local-daemon.js";
7
+ import { daemonBinaryUpdatedAfterStart, localDaemonStatus } from "./local-daemon.js";
6
8
  import { readExistingControllerConfig, readExistingDaemonConfig, relayHealth } from "./relay-http.js";
7
9
  import { startRelayUrl } from "./bootstrap-config.js";
8
10
  export async function topLevelDoctor(flags) {
11
+ const strict = flags.strict === true;
9
12
  const checks = [];
10
13
  const existingController = await readExistingControllerConfig().catch((error) => {
11
- checks.push({ name: "controller-config", ok: false, message: error instanceof Error ? error.message : String(error) });
14
+ checks.push({
15
+ name: "controller-config",
16
+ ok: false,
17
+ severity: "error",
18
+ layer: "compatible",
19
+ message: error instanceof Error ? error.message : String(error),
20
+ });
12
21
  return undefined;
13
22
  });
14
23
  if (existingController) {
15
- checks.push({ name: "controller-config", ok: true, message: configPath, details: redactedControllerConfig(existingController) });
24
+ checks.push({
25
+ name: "controller-config",
26
+ ok: true,
27
+ severity: "info",
28
+ layer: "compatible",
29
+ message: configPath,
30
+ details: redactedControllerConfig(existingController),
31
+ });
16
32
  }
17
33
  const relay = startRelayUrl(flags, existingController);
34
+ let relayReachable = false;
18
35
  try {
19
36
  const health = await relayHealth(relay.relayUrl);
37
+ relayReachable = health.ok;
20
38
  checks.push({
21
39
  name: "relay",
22
40
  ok: health.ok,
41
+ severity: health.ok ? "info" : "error",
42
+ layer: "compatible",
23
43
  message: health.ok ? "reachable" : `HTTP ${health.status}`,
24
44
  details: { relayUrl: relay.relayUrl, body: health.body },
25
45
  });
26
46
  }
27
47
  catch (error) {
28
- checks.push({ name: "relay", ok: false, message: error instanceof Error ? error.message : String(error), details: { relayUrl: relay.relayUrl } });
48
+ checks.push({
49
+ name: "relay",
50
+ ok: false,
51
+ severity: "error",
52
+ layer: "compatible",
53
+ message: error instanceof Error ? error.message : String(error),
54
+ details: { relayUrl: relay.relayUrl },
55
+ });
56
+ }
57
+ let snapshot;
58
+ if (existingController && relayReachable) {
59
+ try {
60
+ snapshot = await new ControllerClient({ ...existingController, relayUrl: relay.relayUrl }).snapshot();
61
+ checks.push({
62
+ name: "controller-snapshot",
63
+ ok: true,
64
+ severity: "info",
65
+ layer: "compatible",
66
+ message: "controller token accepted",
67
+ details: { machines: snapshot.machines.length, sessions: snapshot.sessions.length },
68
+ });
69
+ }
70
+ catch (error) {
71
+ checks.push({
72
+ name: "controller-snapshot",
73
+ ok: false,
74
+ severity: "error",
75
+ layer: "compatible",
76
+ message: error instanceof Error ? error.message : String(error),
77
+ details: { relayUrl: relay.relayUrl },
78
+ });
79
+ }
29
80
  }
30
81
  const existingDaemon = await readExistingDaemonConfig().catch((error) => {
31
- checks.push({ name: "daemon-config", ok: false, message: error instanceof Error ? error.message : String(error) });
82
+ checks.push({
83
+ name: "daemon-config",
84
+ ok: false,
85
+ severity: "error",
86
+ layer: "compatible",
87
+ message: error instanceof Error ? error.message : String(error),
88
+ });
32
89
  return undefined;
33
90
  });
34
91
  if (existingDaemon) {
35
- checks.push({ name: "daemon-config", ok: true, message: path.join(configDir, "daemon.json"), details: redactedDaemonConfig(existingDaemon) });
92
+ checks.push({
93
+ name: "daemon-config",
94
+ ok: true,
95
+ severity: "info",
96
+ layer: "compatible",
97
+ message: path.join(configDir, "daemon.json"),
98
+ details: redactedDaemonConfig(existingDaemon),
99
+ });
36
100
  }
37
101
  const local = await localDaemonStatus();
38
- checks.push({ name: "daemon-process", ok: local.running, message: local.running ? "running" : "not running", details: local });
102
+ checks.push({
103
+ name: "daemon-process",
104
+ ok: local.running,
105
+ severity: local.running ? "info" : "error",
106
+ layer: "compatible",
107
+ message: local.running ? "running" : "not running",
108
+ details: local,
109
+ });
110
+ const binaryStale = await daemonBinaryUpdatedAfterStart(local);
111
+ checks.push({
112
+ name: "daemon-binary-alignment",
113
+ ok: !binaryStale,
114
+ severity: binaryStale ? "warning" : "info",
115
+ layer: "aligned",
116
+ message: binaryStale ? "daemon binary changed after daemon start; restart daemon to align" : "daemon binary is not newer than running daemon",
117
+ });
118
+ if (existingDaemon && snapshot) {
119
+ const daemonMachine = snapshot.machines.find((machine) => machine.id === existingDaemon.machineId);
120
+ const daemonVersion = daemonMachine?.capabilities.daemonVersion;
121
+ const aligned = !daemonVersion || daemonVersion === cliPackage.version;
122
+ checks.push({
123
+ name: "cli-daemon-version-alignment",
124
+ ok: aligned,
125
+ severity: aligned ? "info" : "warning",
126
+ layer: "aligned",
127
+ message: aligned
128
+ ? "CLI and daemon package versions are aligned or daemon version is unavailable"
129
+ : `CLI ${cliPackage.version} differs from daemon ${daemonVersion}`,
130
+ details: { cliVersion: cliPackage.version, daemonVersion, machineId: existingDaemon.machineId },
131
+ });
132
+ }
133
+ const compatible = checks.filter((check) => check.layer === "compatible" && check.severity === "error").every((check) => check.ok);
134
+ const aligned = checks.filter((check) => check.layer === "aligned" && check.severity === "warning").every((check) => check.ok);
135
+ const warnings = checks.filter((check) => !check.ok && check.severity === "warning").map((check) => ({
136
+ name: check.name,
137
+ message: check.message,
138
+ details: check.details,
139
+ }));
140
+ const healthy = compatible && (strict ? aligned : true);
39
141
  if (wantsJson(flags)) {
40
- ok("doctor", { ok: checks.every((check) => check.ok), checks });
142
+ if (!healthy)
143
+ process.exitCode = 1;
144
+ ok("doctor", { ok: healthy, compatible, aligned, strict, warnings, checks }, {}, healthy);
41
145
  return;
42
146
  }
43
- const allOk = checks.every((check) => check.ok);
44
- console.log(`Happy Elves doctor: ${allOk ? "ok" : "issues found"}`);
147
+ console.log(`Happy Elves doctor: ${healthy ? "ok" : "issues found"}`);
148
+ console.log(`compatible: ${compatible ? "yes" : "no"}`);
149
+ console.log(`aligned: ${aligned ? "yes" : "no"}${strict ? " (strict)" : ""}`);
150
+ const failures = checks.filter((check) => !check.ok && check.severity === "error");
151
+ if (failures.length > 0)
152
+ console.log("Failures:");
153
+ for (const check of failures) {
154
+ console.log(`fail ${check.name}: ${check.message}`);
155
+ }
156
+ const warningChecks = checks.filter((check) => !check.ok && check.severity === "warning");
157
+ if (warningChecks.length > 0)
158
+ console.log("Warnings:");
159
+ for (const check of warningChecks) {
160
+ console.log(`warn ${check.name}: ${check.message}`);
161
+ }
162
+ if (failures.length === 0 && warningChecks.length === 0)
163
+ console.log("Checks:");
45
164
  for (const check of checks) {
46
- console.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`);
165
+ if (check.ok)
166
+ console.log(`ok ${check.name}: ${check.message}`);
47
167
  }
168
+ if (!healthy)
169
+ process.exitCode = 1;
48
170
  }
49
171
  //# sourceMappingURL=doctor.js.map
@@ -1,5 +1,5 @@
1
1
  import type { JsonEnvelope } from "../../../../../packages/shared/dist/index.js";
2
- export declare function ok<T>(type: string, data: T, meta?: JsonEnvelope["meta"]): void;
2
+ export declare function ok<T>(type: string, data: T, meta?: JsonEnvelope["meta"], success?: boolean): void;
3
3
  export declare function writeError(code: string, message: string, meta?: JsonEnvelope["meta"], capability?: string): void;
4
4
  export declare function wantsJson(flags: Record<string, string | boolean>): boolean;
5
5
  export declare function wantsVerbose(flags: Record<string, string | boolean>): boolean;
@@ -1,5 +1,8 @@
1
- export function ok(type, data, meta = {}) {
2
- console.log(JSON.stringify({ ok: true, type, data, meta }, null, 2));
1
+ export function ok(type, data, meta = {}, success = true) {
2
+ const envelope = success
3
+ ? { ok: true, type, data, meta }
4
+ : { ok: false, type, data, meta };
5
+ console.log(JSON.stringify(envelope, null, 2));
3
6
  }
4
7
  export function writeError(code, message, meta = {}, capability) {
5
8
  console.error(JSON.stringify({
@@ -43,7 +43,9 @@ export async function topLevelStatus(flags) {
43
43
  } : undefined,
44
44
  };
45
45
  if (wantsJson(flags)) {
46
- ok("status", data);
46
+ if (!data.relay.ok)
47
+ process.exitCode = 1;
48
+ ok("status", data, {}, data.relay.ok);
47
49
  return;
48
50
  }
49
51
  console.log(`Relay: ${config.relayUrl}${relayError ? ` (unreachable: ${relayError})` : ""}`);
@@ -59,5 +61,7 @@ export async function topLevelStatus(flags) {
59
61
  console.log(`Result: happy-elves turn result ${latestSession.id}`);
60
62
  console.log(`Run: happy-elves turn run ${latestSession.id} --text "..."`);
61
63
  }
64
+ if (!data.relay.ok)
65
+ process.exitCode = 1;
62
66
  }
63
67
  //# sourceMappingURL=status.js.map
@@ -107,43 +107,42 @@ Workspace sync records account-level UI/library state shared across devices:
107
107
  session stars, recent sessions, pinned workspaces, and current project defaults.
108
108
  `,
109
109
  gateway: `Usage:
110
- happy-elves gateway enable [<channelId>] --channel fake|lark --machine <machineId> --cwd <cwd> [--agent codex] [--model <model>] [--token <token>] [--secret <secret>] --json
111
- happy-elves gateway disable [<channelId>] --json
112
- happy-elves gateway status [--json]
113
- happy-elves gateway test [<channelId>] --conversation <externalConversationId> --message-id <externalMessageId> --text <prompt> --json
114
- happy-elves gateway serve [--host 127.0.0.1] [--port 8789] --json
110
+ happy-elves gateway enable [<channelId>] --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) [--agent codex] [--model <model>] --json
111
+ happy-elves gateway enable lark-main --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) --app-id <appId> --app-secret-stdin [--domain feishu|lark] [--inbound mention|all] --json
112
+ happy-elves gateway disable [<channelId>] [--machine <machineId>] --json
113
+ happy-elves gateway status [--machine <machineId>] [--include-debug] [--json]
114
+ happy-elves gateway test [<channelId>] [--machine <machineId>] --conversation <externalConversationId> --message-id <externalMessageId> --text <prompt> --json
115
+ happy-elves gateway serve [--host 127.0.0.1] [--port 8789] --json # debug only
115
116
 
116
- Gateway v1 is a thin local entrypoint from external messages into Happy Elves
117
- session create/send. Channel token/secret values stay in the local gateway
118
- config; Relay only receives normal encrypted session and prompt admissions.
117
+ Gateway v1 official Feishu/Lark path is daemon-backed SDK long connection.
118
+ App secrets are read from stdin and stored only in the target daemon config.
119
+ gateway serve is kept for fake/local debugging only; lark-cli is not a product dependency.
119
120
  `,
120
121
  memory: `Usage:
121
- happy-elves memory save --project <projectKey> (--text <text> | --text-file <path>) [--title <title>] [--tags a,b] --json
122
- happy-elves memory list [--project <projectKey>] [--verbose] [--json]
123
- happy-elves memory search --project <projectKey> --query <text> [--limit 5] --json
124
- happy-elves memory delete <memoryId> --json
125
- happy-elves memory status [--project <projectKey>] --json
126
- happy-elves memory enable --project <projectKey> --json
127
- happy-elves memory disable --project <projectKey> --json
128
- happy-elves memory enable-injection --project <projectKey> --json
129
- happy-elves memory disable-injection --project <projectKey> --json
122
+ happy-elves memory overview [--machine <machineId>] [--project <projectKey>] [--cwd <cwd>] --json
123
+ happy-elves memory save [--machine <machineId>] [--project <projectKey>] [--layer user|curated|daily] (--text <text> | --text-file <path>) [--title <title>] [--date YYYY-MM-DD] --json
124
+ happy-elves memory read <USER.md|MEMORY.md|memory/YYYY-MM-DD.md> [--project <projectKey>] --json
125
+ happy-elves memory search --query <text> [--project <projectKey>] [--limit 5] --json
126
+ happy-elves memory reindex [--project <projectKey>] --json
127
+ happy-elves memory sync [--project <projectKey>] --json
130
128
 
131
- Memory v1 is explicit. It never writes automatically. Relay stores encrypted
132
- memory envelopes; search runs after local controller-side decryption.
129
+ Memory uses daemon-local Markdown files: USER.md, MEMORY.md, and memory/YYYY-MM-DD.md.
130
+ Session prompts can run memory off, readonly, or readwrite; readwrite writes daily notes.
133
131
  `,
134
132
  schedule: `Usage:
135
- happy-elves schedule create --session <sessionId> --at <iso-time> (--prompt <text> | --prompt-file <path>) [--name <name>] --json
136
- happy-elves schedule list --json
137
- happy-elves schedule show <scheduleId> --json
138
- happy-elves schedule history <scheduleId> --json
139
- happy-elves schedule pause <scheduleId> --json
140
- happy-elves schedule resume <scheduleId> --json
141
- happy-elves schedule delete <scheduleId> --json
142
- happy-elves schedule run-due --json
143
- happy-elves schedule run-missed <scheduleId> <occurrenceId> --json
133
+ happy-elves schedule create [--session <sessionId> | --cwd <cwd> --agent codex] (--manual | --at <iso-time> | --every 5m | --cron "*/30 * * * *") (--prompt <text> | --prompt-file <path>) [--execution continue|isolated] [--memory inherit|off|readonly|readwrite] [--name <name>] [--machine <machineId>] --json
134
+ happy-elves schedule list [--machine <machineId>] --json
135
+ happy-elves schedule show <scheduleId> [--machine <machineId>] --json
136
+ happy-elves schedule history <scheduleId> [--machine <machineId>] --json
137
+ happy-elves schedule run <scheduleId> [--machine <machineId>] --json
138
+ happy-elves schedule pause <scheduleId> [--machine <machineId>] --json
139
+ happy-elves schedule resume <scheduleId> [--machine <machineId>] --json
140
+ happy-elves schedule delete <scheduleId> [--machine <machineId>] --json
141
+ happy-elves schedule run-due [--machine <machineId>] --json
142
+ happy-elves schedule run-missed <scheduleId> <occurrenceId> [--machine <machineId>] --json
144
143
 
145
- Schedule v1 is explicit one-shot prompt scheduling. Due runs use normal Relay
146
- admission with deterministic requestId/turnId. Offline targets are marked
144
+ Schedule v1 is daemon-local repeat automation. Runs use normal Relay admission
145
+ with deterministic requestId/turnId per occurrence. Offline due times are marked
147
146
  missed and are never auto-caught-up.
148
147
  `,
149
148
  loop: `Usage:
@@ -227,7 +226,7 @@ Core commands:
227
226
  workspace pin-cwd --machine <machineId> --cwd <cwd> --json
228
227
 
229
228
  gateway status --json
230
- gateway enable --channel fake --machine <machineId> --cwd <cwd> --json
229
+ gateway enable --channel fake --machine <machineId> --cwd <cwd> --json # debug only
231
230
  gateway test --conversation <id> --message-id <id> --text <prompt> --json
232
231
 
233
232
  memory save --project <key> --text <fact> --json