@cydm/happy-elves 0.1.0-beta.306 → 0.1.0-beta.308

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.
@@ -118,6 +118,25 @@ export async function handleGateway({ domain, action, positional, flags }) {
118
118
  ok("gateway.test", result);
119
119
  return true;
120
120
  }
121
+ if (action === "notify") {
122
+ const config = await readConfig(flags);
123
+ const client = new ControllerClient(config);
124
+ const machineId = await resolveGatewayMachineId(client, flags);
125
+ const channelId = gatewayChannelId(flags, positional[0], "lark");
126
+ const externalConversationId = requireString(flags, "conversation");
127
+ const externalMessageId = typeof flags["message-id"] === "string" ? flags["message-id"] : `notify_${Date.now()}`;
128
+ const text = requireString(flags, "text");
129
+ const result = await client.gateway({
130
+ machineId,
131
+ action: "notify",
132
+ channelId,
133
+ externalConversationId,
134
+ externalMessageId,
135
+ text,
136
+ });
137
+ ok("gateway.notify", result);
138
+ return true;
139
+ }
121
140
  if (action === "serve") {
122
141
  const config = await readConfig(flags);
123
142
  const port = Number(typeof flags.port === "string" ? flags.port : "8789");
@@ -119,6 +119,7 @@ session stars, recent sessions, pinned workspaces, and current project defaults.
119
119
  happy-elves gateway disable [<channelId>] [--machine <machineId>] --json
120
120
  happy-elves gateway status [--machine <machineId>] [--include-debug] [--json]
121
121
  happy-elves gateway test [<channelId>] [--machine <machineId>] --conversation <externalConversationId> --message-id <externalMessageId> --text <prompt> --json
122
+ happy-elves gateway notify [<channelId>] [--machine <machineId>] --conversation <externalConversationId> [--message-id <id>] --text <message> --json
122
123
  happy-elves gateway serve [--host 127.0.0.1] [--port 8789] --json # debug only
123
124
 
124
125
  Gateway v1 official Feishu/Lark path is daemon-backed SDK long connection.
@@ -253,6 +254,7 @@ Core commands:
253
254
  gateway status --json
254
255
  gateway enable --channel fake --machine <machineId> --cwd <cwd> --json # debug only
255
256
  gateway test --conversation <id> --message-id <id> --text <prompt> --json
257
+ gateway notify --conversation <id> --text <message> --json
256
258
 
257
259
  memory save --project <key> --text <fact> --json
258
260
  memory search --project <key> --query <prompt> --json
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.306",
3
+ "version": "0.1.0-beta.308",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  import { decryptJson } from "../../../../packages/shared/dist/index.js";
2
2
  import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
3
- import { deliverGatewayMessage, publicChannelsForStore, setGatewayLastError, syncGatewayRuntime } from "./runtime.js";
3
+ import { deliverGatewayMessage, notifyGatewayConversation, publicChannelsForStore, setGatewayLastError, syncGatewayRuntime } from "./runtime.js";
4
4
  import { readGatewayStore, updateGatewayStore, upsertGatewayChannel } from "./store.js";
5
5
  export async function handleGatewayCommand(ws, config, command) {
6
6
  if (!claimCommandRequest(ws, command.requestId))
@@ -86,6 +86,32 @@ async function executeGatewayCommand(config, command) {
86
86
  throw error;
87
87
  }
88
88
  }
89
+ if (command.action === "notify") {
90
+ const store = await readGatewayStore();
91
+ const channel = requireChannel(store, command.channelId);
92
+ if (!channel.enabled)
93
+ throw new Error("Gateway channel is disabled");
94
+ if (!command.externalConversationId || !command.externalMessageId || !command.text) {
95
+ throw new Error("gateway notify requires externalConversationId, externalMessageId, and text");
96
+ }
97
+ try {
98
+ const delivery = await notifyGatewayConversation(channel, {
99
+ externalConversationId: command.externalConversationId,
100
+ externalMessageId: command.externalMessageId,
101
+ text: command.text,
102
+ });
103
+ const nextStore = await readGatewayStore();
104
+ return {
105
+ action: "notify",
106
+ channel: publicChannelsForStore(nextStore).find((item) => item.id === channel.id),
107
+ delivery,
108
+ };
109
+ }
110
+ catch (error) {
111
+ await setGatewayLastError(channel.id, error);
112
+ throw error;
113
+ }
114
+ }
89
115
  throw new Error(`Unsupported gateway action: ${command.action}`);
90
116
  }
91
117
  async function decryptGatewayChannelInput(config, input) {
@@ -22,6 +22,11 @@ export declare function startGatewayRuntime(config: DaemonConfig): Promise<void>
22
22
  export declare function stopGatewayRuntime(): Promise<void>;
23
23
  export declare function syncGatewayRuntime(config?: DaemonConfig | undefined): Promise<void>;
24
24
  export declare function deliverGatewayMessage(config: DaemonConfig, channel: GatewayChannelConfig, message: GatewayInboundMessage): Promise<GatewayDeliveryResult>;
25
+ export declare function notifyGatewayConversation(channel: GatewayChannelConfig, input: {
26
+ externalConversationId: string;
27
+ externalMessageId: string;
28
+ text: string;
29
+ }): Promise<GatewayDeliveryResult>;
25
30
  export declare function handleGatewayTurnTerminal(config: DaemonConfig, input: GatewayTerminalInput): Promise<void>;
26
31
  export declare function setGatewayLastError(channelId: string, error: unknown): Promise<void>;
27
32
  export declare function publicChannelsForStore(store: GatewayStore): import("../../../../packages/shared/dist/index.js").GatewayChannelPublic[];
@@ -229,6 +229,58 @@ export async function deliverGatewayMessage(config, channel, message) {
229
229
  senderDisplayName: message.senderDisplayName,
230
230
  });
231
231
  }
232
+ export async function notifyGatewayConversation(channel, input) {
233
+ if (!channel.enabled)
234
+ throw gatewayError("Gateway channel is disabled", "GATEWAY_DISABLED", 400);
235
+ const adapter = gatewayAdapters.get(channel.id)?.adapter;
236
+ if (!adapter)
237
+ throw gatewayError("Gateway adapter is not listening", "GATEWAY_NOT_LISTENING", 503);
238
+ const destination = destinationFromGatewayConversationId(input.externalConversationId);
239
+ const deliveryId = stableGatewayId("gw_notify", channel.id, input.externalConversationId, input.externalMessageId);
240
+ const result = await adapter.send({
241
+ deliveryId,
242
+ destination,
243
+ payload: {
244
+ type: "notice",
245
+ title: "Happy Elves",
246
+ text: input.text,
247
+ severity: "info",
248
+ },
249
+ });
250
+ const repliedAt = new Date().toISOString();
251
+ const lastReplied = {
252
+ externalConversationId: input.externalConversationId,
253
+ externalMessageId: input.externalMessageId,
254
+ deliveryId,
255
+ platformMessageId: result.platformMessageId,
256
+ at: repliedAt,
257
+ };
258
+ await updateGatewayStore((current) => ({
259
+ ...current,
260
+ channels: current.channels.map((item) => item.id === channel.id
261
+ ? { ...item, lastError: undefined, lastReplied, updatedAt: repliedAt }
262
+ : item),
263
+ replyHistory: [
264
+ {
265
+ channelId: channel.id,
266
+ externalConversationId: input.externalConversationId,
267
+ externalMessageId: input.externalMessageId,
268
+ deliveryId,
269
+ platformMessageId: result.platformMessageId,
270
+ repliedAt,
271
+ },
272
+ ...current.replyHistory.filter((item) => !(item.channelId === channel.id && item.deliveryId === deliveryId)),
273
+ ].slice(0, gatewayReplyHistoryLimit),
274
+ }));
275
+ return {
276
+ channelId: channel.id,
277
+ externalConversationId: input.externalConversationId,
278
+ externalMessageId: input.externalMessageId,
279
+ deliveryId,
280
+ platformMessageId: result.platformMessageId,
281
+ dispatch: "delivered",
282
+ };
283
+ }
232
284
  export async function handleGatewayTurnTerminal(config, input) {
233
285
  await deliverPendingGatewayReply(config, input);
234
286
  await drainGatewayQueue(config, input.sessionId);
@@ -240,16 +292,17 @@ async function admitGatewayPromptWithMappingRecovery(config, channel, mapping, i
240
292
  catch (error) {
241
293
  if (!canRecoverGatewayMapping(channel, error))
242
294
  throw error;
243
- const recovered = await recoverGatewayMapping(config, channel, mapping, input.externalConversationId);
295
+ const recovered = await recoverGatewayMapping(config, channel, mapping, input.externalConversationId, input.requestId);
244
296
  return await admitGatewayPromptNow(config, channel, {
245
297
  ...input,
246
298
  sessionId: recovered.sessionId,
247
299
  });
248
300
  }
249
301
  }
250
- async function recoverGatewayMapping(config, channel, staleMapping, externalConversationId) {
251
- const createRequestId = stableGatewayId("gw_create", channel.id, externalConversationId);
252
- const mapping = await createGatewayMapping(config, channel, externalConversationId, createRequestId);
302
+ async function recoverGatewayMapping(config, channel, staleMapping, externalConversationId, recoverySeed) {
303
+ const createRequestId = stableGatewayId("gw_create", channel.id, externalConversationId, "recover", recoverySeed);
304
+ const sessionId = stableGatewayId("ses_gw", channel.id, externalConversationId, "recover", recoverySeed);
305
+ const mapping = await createGatewayMapping(config, channel, externalConversationId, createRequestId, sessionId);
253
306
  if (mapping.sessionId === staleMapping.sessionId)
254
307
  return mapping;
255
308
  const now = new Date().toISOString();
@@ -266,9 +319,9 @@ function canRecoverGatewayMapping(channel, error) {
266
319
  return false;
267
320
  if (!channel.targetCwd)
268
321
  return false;
269
- return errorCode(error) === "SESSION_NOT_FOUND";
322
+ return errorCode(error) === "SESSION_NOT_FOUND" || errorCode(error) === "SESSION_CLOSED";
270
323
  }
271
- async function createGatewayMapping(config, channel, externalConversationId, createRequestId) {
324
+ async function createGatewayMapping(config, channel, externalConversationId, createRequestId, recoveredSessionId) {
272
325
  if (channel.targetSessionId) {
273
326
  const now = new Date().toISOString();
274
327
  const mapping = {
@@ -287,7 +340,7 @@ async function createGatewayMapping(config, channel, externalConversationId, cre
287
340
  }
288
341
  if (!channel.targetCwd)
289
342
  throw gatewayError("Gateway channel needs target cwd or target session", "GATEWAY_TARGET_MISSING", 400);
290
- const sessionId = stableGatewayId("ses_gw", channel.id, externalConversationId);
343
+ const sessionId = recoveredSessionId ?? stableGatewayId("ses_gw", channel.id, externalConversationId);
291
344
  const create = await admitMachineCreateSession(config, {
292
345
  requestId: createRequestId,
293
346
  sessionId,
@@ -629,6 +682,16 @@ function gatewayConversationId(destination) {
629
682
  return `${destination.platformChatId}:thread:${destination.contextKey}`;
630
683
  return `${destination.platformChatId}:main`;
631
684
  }
685
+ function destinationFromGatewayConversationId(externalConversationId) {
686
+ const threadMatch = /^(.+):thread:(.+)$/.exec(externalConversationId);
687
+ if (threadMatch) {
688
+ return { platformChatId: threadMatch[1], contextKind: "thread", contextKey: threadMatch[2] };
689
+ }
690
+ const mainMatch = /^(.+):main$/.exec(externalConversationId);
691
+ if (mainMatch)
692
+ return { platformChatId: mainMatch[1], contextKind: "main" };
693
+ throw gatewayError("Gateway notify requires a conversation id like <chatId>:main or <chatId>:thread:<threadId>", "GATEWAY_CONVERSATION_INVALID", 400);
694
+ }
632
695
  function gatewayAdapterKey(channel) {
633
696
  return JSON.stringify({
634
697
  type: channel.type,
@@ -70,9 +70,9 @@ type GatewayReplyHistory = {
70
70
  channelId: string;
71
71
  externalConversationId: string;
72
72
  externalMessageId: string;
73
- sessionId: string;
74
- requestId: string;
75
- turnId: string;
73
+ sessionId?: string;
74
+ requestId?: string;
75
+ turnId?: string;
76
76
  deliveryId: string;
77
77
  platformMessageId?: string;
78
78
  repliedAt: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.306",
3
+ "version": "0.1.0-beta.308",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.306",
3
+ "version": "0.1.0-beta.308",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -297,7 +297,7 @@ export type MachineDevExecInput = {
297
297
  };
298
298
  export type GatewayCommandInput = {
299
299
  machineId: string;
300
- action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
300
+ action: "list" | "get" | "upsert" | "enable" | "disable" | "test" | "notify";
301
301
  channelId?: string;
302
302
  channel?: GatewayChannelInput;
303
303
  externalConversationId?: string;
@@ -552,9 +552,11 @@ export declare const gatewayDeliveryResultSchema: z.ZodObject<{
552
552
  channelId: z.ZodString;
553
553
  externalConversationId: z.ZodString;
554
554
  externalMessageId: z.ZodString;
555
- sessionId: z.ZodString;
556
- requestId: z.ZodString;
557
- turnId: z.ZodString;
555
+ sessionId: z.ZodOptional<z.ZodString>;
556
+ requestId: z.ZodOptional<z.ZodString>;
557
+ turnId: z.ZodOptional<z.ZodString>;
558
+ deliveryId: z.ZodOptional<z.ZodString>;
559
+ platformMessageId: z.ZodOptional<z.ZodString>;
558
560
  admission: z.ZodOptional<z.ZodEnum<{
559
561
  accepted: "accepted";
560
562
  replayed: "replayed";
@@ -574,6 +576,7 @@ export declare const gatewayCommandResultSchema: z.ZodObject<{
574
576
  enable: "enable";
575
577
  disable: "disable";
576
578
  test: "test";
579
+ notify: "notify";
577
580
  }>;
578
581
  channels: z.ZodOptional<z.ZodArray<z.ZodObject<{
579
582
  id: z.ZodString;
@@ -727,9 +730,11 @@ export declare const gatewayCommandResultSchema: z.ZodObject<{
727
730
  channelId: z.ZodString;
728
731
  externalConversationId: z.ZodString;
729
732
  externalMessageId: z.ZodString;
730
- sessionId: z.ZodString;
731
- requestId: z.ZodString;
732
- turnId: z.ZodString;
733
+ sessionId: z.ZodOptional<z.ZodString>;
734
+ requestId: z.ZodOptional<z.ZodString>;
735
+ turnId: z.ZodOptional<z.ZodString>;
736
+ deliveryId: z.ZodOptional<z.ZodString>;
737
+ platformMessageId: z.ZodOptional<z.ZodString>;
733
738
  admission: z.ZodOptional<z.ZodEnum<{
734
739
  accepted: "accepted";
735
740
  replayed: "replayed";
@@ -281,16 +281,18 @@ export const gatewayDeliveryResultSchema = z.object({
281
281
  channelId: z.string().min(1),
282
282
  externalConversationId: z.string().min(1),
283
283
  externalMessageId: z.string().min(1),
284
- sessionId: z.string().min(1),
285
- requestId: z.string().min(1),
286
- turnId: z.string().min(1),
284
+ sessionId: z.string().min(1).optional(),
285
+ requestId: z.string().min(1).optional(),
286
+ turnId: z.string().min(1).optional(),
287
+ deliveryId: z.string().min(1).optional(),
288
+ platformMessageId: z.string().min(1).optional(),
287
289
  admission: z.enum(["accepted", "replayed"]).optional(),
288
290
  dispatch: z.enum(["queued", "delivered"]).optional(),
289
291
  queued: z.boolean().optional(),
290
292
  duplicate: z.boolean().optional(),
291
293
  });
292
294
  export const gatewayCommandResultSchema = z.object({
293
- action: z.enum(["list", "get", "upsert", "enable", "disable", "test"]),
295
+ action: z.enum(["list", "get", "upsert", "enable", "disable", "test", "notify"]),
294
296
  channels: z.array(gatewayChannelPublicSchema).optional(),
295
297
  channel: gatewayChannelPublicSchema.optional(),
296
298
  delivery: gatewayDeliveryResultSchema.optional(),
@@ -295,16 +295,18 @@ export type GatewayDeliveryResult = {
295
295
  channelId: string;
296
296
  externalConversationId: string;
297
297
  externalMessageId: string;
298
- sessionId: string;
299
- requestId: string;
300
- turnId: string;
298
+ sessionId?: string;
299
+ requestId?: string;
300
+ turnId?: string;
301
+ deliveryId?: string;
302
+ platformMessageId?: string;
301
303
  admission?: "accepted" | "replayed";
302
304
  dispatch?: "queued" | "delivered";
303
305
  queued?: boolean;
304
306
  duplicate?: boolean;
305
307
  };
306
308
  export type GatewayCommandResult = {
307
- action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
309
+ action: "list" | "get" | "upsert" | "enable" | "disable" | "test" | "notify";
308
310
  channels?: GatewayChannelPublic[];
309
311
  channel?: GatewayChannelPublic;
310
312
  delivery?: GatewayDeliveryResult;
@@ -85,7 +85,7 @@ export type ControllerClientMessage = {
85
85
  type: "controller:gateway";
86
86
  requestId: string;
87
87
  machineId: string;
88
- action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
88
+ action: "list" | "get" | "upsert" | "enable" | "disable" | "test" | "notify";
89
89
  channelId?: string;
90
90
  channel?: GatewayChannelInput;
91
91
  externalConversationId?: string;
@@ -398,7 +398,7 @@ export type MachineCommand = {
398
398
  type: "machine:gateway";
399
399
  requestId: string;
400
400
  machineId: string;
401
- action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
401
+ action: "list" | "get" | "upsert" | "enable" | "disable" | "test" | "notify";
402
402
  channelId?: string;
403
403
  channel?: GatewayChannelInput;
404
404
  externalConversationId?: string;
@@ -97,7 +97,7 @@ const controllerMessageSchema = z.discriminatedUnion("type", [
97
97
  type: z.literal("controller:gateway"),
98
98
  requestId: z.string().min(1),
99
99
  machineId: z.string().min(1),
100
- action: z.enum(["list", "get", "upsert", "enable", "disable", "test"]),
100
+ action: z.enum(["list", "get", "upsert", "enable", "disable", "test", "notify"]),
101
101
  channelId: z.string().min(1).optional(),
102
102
  channel: gatewayChannelInputSchema.optional(),
103
103
  externalConversationId: z.string().min(1).optional(),
@@ -473,7 +473,7 @@ const machineCommandSchema = z.discriminatedUnion("type", [
473
473
  type: z.literal("machine:gateway"),
474
474
  requestId: z.string().min(1),
475
475
  machineId: z.string().min(1),
476
- action: z.enum(["list", "get", "upsert", "enable", "disable", "test"]),
476
+ action: z.enum(["list", "get", "upsert", "enable", "disable", "test", "notify"]),
477
477
  channelId: z.string().min(1).optional(),
478
478
  channel: gatewayChannelInputSchema.optional(),
479
479
  externalConversationId: z.string().min(1).optional(),