@cydm/happy-elves 0.1.0-beta.83 → 0.1.0-beta.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/cli/dist/commands/lib/args.js +2 -0
- package/apps/cli/dist/commands/lib/usage.js +2 -1
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/gateway/store.js +146 -3
- package/apps/daemon/dist/session/events.d.ts +1 -1
- package/apps/daemon/dist/session/events.js +1 -1
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/db.js +40 -2
- package/package.json +2 -1
|
@@ -108,7 +108,7 @@ session stars, recent sessions, pinned workspaces, and current project defaults.
|
|
|
108
108
|
`,
|
|
109
109
|
gateway: `Usage:
|
|
110
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
|
|
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] (--allow-any-sender | --allow-chat <id[,id]> | --allow-sender <id[,id]>) --json
|
|
112
112
|
happy-elves gateway disable [<channelId>] [--machine <machineId>] --json
|
|
113
113
|
happy-elves gateway status [--machine <machineId>] [--include-debug] [--json]
|
|
114
114
|
happy-elves gateway test [<channelId>] [--machine <machineId>] --conversation <externalConversationId> --message-id <externalMessageId> --text <prompt> --json
|
|
@@ -116,6 +116,7 @@ session stars, recent sessions, pinned workspaces, and current project defaults.
|
|
|
116
116
|
|
|
117
117
|
Gateway v1 official Feishu/Lark path is daemon-backed SDK long connection.
|
|
118
118
|
App secrets are read from stdin and stored only in the target daemon config.
|
|
119
|
+
Lark/Feishu product gateways require an explicit inbound sender/chat policy.
|
|
119
120
|
gateway serve is kept for fake/local debugging only; lark-cli is not a product dependency.
|
|
120
121
|
`,
|
|
121
122
|
memory: `Usage:
|
package/apps/cli/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { configDir, gatewayStorePath } from "../paths.js";
|
|
3
5
|
function emptyGatewayStore() {
|
|
4
6
|
return { version: 1, channels: [], mappings: [], queue: [], pendingReplies: [], replyHistory: [] };
|
|
5
7
|
}
|
|
6
8
|
export async function readGatewayStore() {
|
|
9
|
+
let store;
|
|
7
10
|
try {
|
|
8
11
|
const parsed = JSON.parse(await fs.readFile(gatewayStorePath, "utf8"));
|
|
9
12
|
if (parsed.version !== 1 || !Array.isArray(parsed.channels) || !Array.isArray(parsed.mappings))
|
|
10
|
-
return emptyGatewayStore();
|
|
11
|
-
|
|
13
|
+
return await mergeLegacyGatewayStore(emptyGatewayStore());
|
|
14
|
+
store = {
|
|
12
15
|
version: 1,
|
|
13
16
|
channels: parsed.channels.map(normalizeGatewayChannel),
|
|
14
17
|
mappings: parsed.mappings,
|
|
@@ -19,9 +22,10 @@ export async function readGatewayStore() {
|
|
|
19
22
|
}
|
|
20
23
|
catch (error) {
|
|
21
24
|
if (error.code === "ENOENT")
|
|
22
|
-
return emptyGatewayStore();
|
|
25
|
+
return await mergeLegacyGatewayStore(emptyGatewayStore());
|
|
23
26
|
throw error;
|
|
24
27
|
}
|
|
28
|
+
return await mergeLegacyGatewayStore(store);
|
|
25
29
|
}
|
|
26
30
|
async function writeGatewayStore(store) {
|
|
27
31
|
await fs.mkdir(configDir, { recursive: true });
|
|
@@ -122,4 +126,143 @@ function normalizeGatewayChannel(channel) {
|
|
|
122
126
|
allowedSenderIds: Array.isArray(channel.allowedSenderIds) ? channel.allowedSenderIds.filter(Boolean) : undefined,
|
|
123
127
|
};
|
|
124
128
|
}
|
|
129
|
+
async function mergeLegacyGatewayStore(store) {
|
|
130
|
+
const legacy = await readLegacyGatewayStore();
|
|
131
|
+
if (!legacy)
|
|
132
|
+
return store;
|
|
133
|
+
let changed = false;
|
|
134
|
+
let channels = store.channels;
|
|
135
|
+
for (const legacyChannel of legacy.channels) {
|
|
136
|
+
if (channels.some((channel) => channel.id === legacyChannel.id))
|
|
137
|
+
continue;
|
|
138
|
+
if (channels.some((channel) => channel.type === "lark" && channel.enabled && channel.appId === legacyChannel.appId))
|
|
139
|
+
continue;
|
|
140
|
+
channels = [legacyChannel, ...channels];
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
const channelIds = new Set(channels.map((channel) => channel.id));
|
|
144
|
+
let mappings = store.mappings;
|
|
145
|
+
for (const legacyMapping of legacy.mappings) {
|
|
146
|
+
if (!channelIds.has(legacyMapping.channelId))
|
|
147
|
+
continue;
|
|
148
|
+
if (mappings.some((mapping) => mapping.channelId === legacyMapping.channelId
|
|
149
|
+
&& mapping.externalConversationId === legacyMapping.externalConversationId))
|
|
150
|
+
continue;
|
|
151
|
+
mappings = [legacyMapping, ...mappings];
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
154
|
+
if (!changed)
|
|
155
|
+
return store;
|
|
156
|
+
const next = { ...store, channels, mappings };
|
|
157
|
+
await writeGatewayStore(next);
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
async function readLegacyGatewayStore() {
|
|
161
|
+
const legacyConfig = await readJsonFile(path.join(configDir, "gateways.json"));
|
|
162
|
+
const legacyGateways = Array.isArray(legacyConfig?.gateways) ? legacyConfig.gateways : [];
|
|
163
|
+
if (legacyGateways.length === 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
const legacyState = await readJsonFile(path.join(configDir, "gateway-state.json"));
|
|
166
|
+
const legacyBindings = Array.isArray(legacyState?.bindings) ? legacyState.bindings : [];
|
|
167
|
+
const channels = legacyGateways
|
|
168
|
+
.map(legacyChannelFromRecord)
|
|
169
|
+
.filter((channel) => Boolean(channel));
|
|
170
|
+
const channelIds = new Set(channels.map((channel) => channel.id));
|
|
171
|
+
const mappings = legacyBindings
|
|
172
|
+
.map(legacyMappingFromRecord)
|
|
173
|
+
.filter((mapping) => mapping !== undefined && channelIds.has(mapping.channelId));
|
|
174
|
+
return channels.length > 0 || mappings.length > 0 ? { channels, mappings } : undefined;
|
|
175
|
+
}
|
|
176
|
+
async function readJsonFile(filePath) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
179
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code === "ENOENT")
|
|
183
|
+
return undefined;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function legacyChannelFromRecord(value) {
|
|
188
|
+
const record = isRecord(value) ? value : undefined;
|
|
189
|
+
if (!record)
|
|
190
|
+
return undefined;
|
|
191
|
+
const platform = stringField(record, "platform");
|
|
192
|
+
if (platform !== "feishu" && platform !== "lark")
|
|
193
|
+
return undefined;
|
|
194
|
+
const id = stringField(record, "id");
|
|
195
|
+
if (!id)
|
|
196
|
+
return undefined;
|
|
197
|
+
const identities = Array.isArray(record.identities) ? record.identities.filter(isRecord) : [];
|
|
198
|
+
const allowedSenderIds = uniqueStrings(identities.map((identity) => stringField(identity, "platformUserId")));
|
|
199
|
+
const allowAnySender = booleanField(record, "allowAllUsers") ?? allowedSenderIds.length === 0;
|
|
200
|
+
const createdAt = legacyIsoDate(record.createdAt);
|
|
201
|
+
const updatedAt = legacyIsoDate(record.updatedAt, createdAt);
|
|
202
|
+
return normalizeGatewayChannel({
|
|
203
|
+
id,
|
|
204
|
+
type: "lark",
|
|
205
|
+
enabled: booleanField(record, "enabled") ?? true,
|
|
206
|
+
domain: gatewayDomain(stringField(record, "domain") ?? platform),
|
|
207
|
+
appId: stringField(record, "appId"),
|
|
208
|
+
appSecret: stringField(record, "appSecret"),
|
|
209
|
+
inboundMode: booleanField(record, "requireMention") === false ? "all" : "mention",
|
|
210
|
+
processingReaction: { enabled: true, emojiType: "Typing" },
|
|
211
|
+
targetCwd: stringField(record, "defaultCwd"),
|
|
212
|
+
defaultAgent: stringField(record, "defaultAgent") ?? "codex",
|
|
213
|
+
allowAnySender,
|
|
214
|
+
allowedSenderIds: allowAnySender ? undefined : allowedSenderIds,
|
|
215
|
+
createdAt,
|
|
216
|
+
updatedAt,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function legacyMappingFromRecord(value) {
|
|
220
|
+
const record = isRecord(value) ? value : undefined;
|
|
221
|
+
if (!record)
|
|
222
|
+
return undefined;
|
|
223
|
+
const channelId = stringField(record, "gatewayId");
|
|
224
|
+
const platformChatId = stringField(record, "platformChatId");
|
|
225
|
+
const sessionId = stringField(record, "sessionId");
|
|
226
|
+
if (!channelId || !platformChatId || !sessionId)
|
|
227
|
+
return undefined;
|
|
228
|
+
const contextKind = stringField(record, "contextKind");
|
|
229
|
+
const contextKey = stringField(record, "contextKey");
|
|
230
|
+
const externalConversationId = contextKind === "thread" && contextKey
|
|
231
|
+
? `${platformChatId}:thread:${contextKey}`
|
|
232
|
+
: `${platformChatId}:main`;
|
|
233
|
+
const createdAt = legacyIsoDate(record.createdAt);
|
|
234
|
+
return {
|
|
235
|
+
channelId,
|
|
236
|
+
externalConversationId,
|
|
237
|
+
sessionId,
|
|
238
|
+
createRequestId: stableLegacyGatewayId("gw_create", channelId, externalConversationId),
|
|
239
|
+
createdAt,
|
|
240
|
+
updatedAt: legacyIsoDate(record.updatedAt, createdAt),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function stableLegacyGatewayId(prefix, ...parts) {
|
|
244
|
+
return `${prefix}_${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 32)}`;
|
|
245
|
+
}
|
|
246
|
+
function gatewayDomain(value) {
|
|
247
|
+
return value === "lark" ? "lark" : "feishu";
|
|
248
|
+
}
|
|
249
|
+
function legacyIsoDate(value, fallback = new Date().toISOString()) {
|
|
250
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
251
|
+
const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(fallback);
|
|
252
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : new Date().toISOString();
|
|
253
|
+
}
|
|
254
|
+
function uniqueStrings(values) {
|
|
255
|
+
return Array.from(new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value))));
|
|
256
|
+
}
|
|
257
|
+
function stringField(record, key) {
|
|
258
|
+
const value = record[key];
|
|
259
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
260
|
+
}
|
|
261
|
+
function booleanField(record, key) {
|
|
262
|
+
const value = record[key];
|
|
263
|
+
return typeof value === "boolean" ? value : undefined;
|
|
264
|
+
}
|
|
265
|
+
function isRecord(value) {
|
|
266
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
267
|
+
}
|
|
125
268
|
//# sourceMappingURL=store.js.map
|
|
@@ -25,7 +25,7 @@ export type HistoricalSessionHead = {
|
|
|
25
25
|
lastTurnId: string;
|
|
26
26
|
basis: SessionHeadAdvanceBasis;
|
|
27
27
|
};
|
|
28
|
-
export declare function latestCompletedHistoricalHead(sessionId: string, events: RuntimeHistoricalEvent[],
|
|
28
|
+
export declare function latestCompletedHistoricalHead(sessionId: string, events: RuntimeHistoricalEvent[], _input?: {
|
|
29
29
|
preferTailBasisForExistingEvents?: boolean;
|
|
30
30
|
previousTotalEvents?: number;
|
|
31
31
|
tailStartIndex?: number;
|
|
@@ -100,7 +100,7 @@ function transcriptHead(events) {
|
|
|
100
100
|
}
|
|
101
101
|
return events.at(-1)?.turnId ? { currentHead: events.at(-1)?.turnId, lastTurnId: events.at(-1)?.turnId } : {};
|
|
102
102
|
}
|
|
103
|
-
export function latestCompletedHistoricalHead(sessionId, events,
|
|
103
|
+
export function latestCompletedHistoricalHead(sessionId, events, _input = {}) {
|
|
104
104
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
105
105
|
const event = events[index];
|
|
106
106
|
if (!event || event.payload.type !== "done" || event.payload.status !== "completed")
|
package/apps/daemon/package.json
CHANGED
package/apps/relay/dist/db.js
CHANGED
|
@@ -291,6 +291,9 @@ export function initDb(db) {
|
|
|
291
291
|
ensureColumn(db, "relay_admissions", "finished_at", "INTEGER");
|
|
292
292
|
ensureColumn(db, "relay_admissions", "terminal_status", "TEXT");
|
|
293
293
|
ensureColumn(db, "relay_admissions", "last_error_code", "TEXT");
|
|
294
|
+
ensureColumn(db, "relay_dispatch_outbox", "message", "TEXT");
|
|
295
|
+
ensureColumn(db, "relay_dispatch_outbox", "command_type", "TEXT");
|
|
296
|
+
ensureColumn(db, "relay_dispatch_outbox", "delivered_at", "INTEGER");
|
|
294
297
|
ensureColumn(db, "tokens", "device_name", "TEXT");
|
|
295
298
|
ensureColumn(db, "tokens", "revoked_at", "INTEGER");
|
|
296
299
|
ensureColumn(db, "tokens", "expires_at", "INTEGER");
|
|
@@ -312,6 +315,7 @@ export function initDb(db) {
|
|
|
312
315
|
WHERE message_id IS NOT NULL;
|
|
313
316
|
`);
|
|
314
317
|
repairHistoricalEventTimestamps(db);
|
|
318
|
+
backfillRelayDispatchOutboxCommandType(db);
|
|
315
319
|
restoreDurableSessionClaims(db);
|
|
316
320
|
}
|
|
317
321
|
export function pruneRelayRetention(db, now, config) {
|
|
@@ -323,11 +327,13 @@ export function pruneRelayRetention(db, now, config) {
|
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
function ensureColumn(db, table, column, definition) {
|
|
326
|
-
|
|
327
|
-
if (!rows.some((row) => row.name === column)) {
|
|
330
|
+
if (!tableColumnNames(db, table).has(column)) {
|
|
328
331
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
329
332
|
}
|
|
330
333
|
}
|
|
334
|
+
function tableColumnNames(db, table) {
|
|
335
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
336
|
+
}
|
|
331
337
|
function restoreDurableSessionClaims(db) {
|
|
332
338
|
db.prepare(`INSERT OR IGNORE INTO relay_session_claims
|
|
333
339
|
(account_id, session_id, request_id, machine_id, turn_id, command_type, state, created_at, updated_at)
|
|
@@ -393,6 +399,38 @@ function repairHistoricalEventTimestamps(db) {
|
|
|
393
399
|
throw error;
|
|
394
400
|
}
|
|
395
401
|
}
|
|
402
|
+
function backfillRelayDispatchOutboxCommandType(db) {
|
|
403
|
+
const columns = tableColumnNames(db, "relay_dispatch_outbox");
|
|
404
|
+
if (columns.has("command_json")) {
|
|
405
|
+
db.prepare("UPDATE relay_dispatch_outbox SET message = command_json WHERE (message IS NULL OR message = '') AND command_json IS NOT NULL AND command_json != ''").run();
|
|
406
|
+
}
|
|
407
|
+
const rows = db
|
|
408
|
+
.prepare("SELECT rowid, message FROM relay_dispatch_outbox WHERE command_type IS NULL OR command_type = ''")
|
|
409
|
+
.all();
|
|
410
|
+
if (rows.length === 0)
|
|
411
|
+
return;
|
|
412
|
+
const update = db.prepare("UPDATE relay_dispatch_outbox SET command_type = ? WHERE rowid = ?");
|
|
413
|
+
db.exec("BEGIN");
|
|
414
|
+
try {
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
update.run(commandTypeFromOutboxMessage(row.message ?? ""), row.rowid);
|
|
417
|
+
}
|
|
418
|
+
db.exec("COMMIT");
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
db.exec("ROLLBACK");
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function commandTypeFromOutboxMessage(message) {
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(message);
|
|
428
|
+
return typeof parsed.type === "string" && parsed.type.trim() ? parsed.type : "unknown";
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return "unknown";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
396
434
|
function uuidV7TimestampMs(value) {
|
|
397
435
|
if (!value)
|
|
398
436
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cydm/happy-elves",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.84",
|
|
4
4
|
"description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@fastify/cors": "^11.1.0",
|
|
30
30
|
"@fastify/websocket": "^11.0.2",
|
|
31
|
+
"@larksuiteoapi/node-sdk": "^1.72.0",
|
|
31
32
|
"@noble/ciphers": "^2.2.0",
|
|
32
33
|
"@noble/hashes": "^2.2.0",
|
|
33
34
|
"acpx": "^0.13.0",
|