@rynx-ai/plugin-channel-lark 0.1.9

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 (41) hide show
  1. package/dist/cli-mirror/mirror-index.d.ts +46 -0
  2. package/dist/cli-mirror/mirror-index.js +48 -0
  3. package/dist/cli-mirror/rollout-mirror.d.ts +85 -0
  4. package/dist/cli-mirror/rollout-mirror.js +333 -0
  5. package/dist/cli-mirror/rollout-watcher.d.ts +109 -0
  6. package/dist/cli-mirror/rollout-watcher.js +347 -0
  7. package/dist/host-adapters.d.ts +82 -0
  8. package/dist/host-adapters.js +404 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +7 -0
  11. package/dist/lark-card-stream.d.ts +363 -0
  12. package/dist/lark-card-stream.js +1335 -0
  13. package/dist/lark-content.d.ts +30 -0
  14. package/dist/lark-content.js +138 -0
  15. package/dist/lark-conversation-directory.d.ts +65 -0
  16. package/dist/lark-conversation-directory.js +106 -0
  17. package/dist/lark-diff-card.d.ts +25 -0
  18. package/dist/lark-diff-card.js +58 -0
  19. package/dist/lark-fork-transcript.d.ts +16 -0
  20. package/dist/lark-fork-transcript.js +141 -0
  21. package/dist/lark-resume-card.d.ts +31 -0
  22. package/dist/lark-resume-card.js +105 -0
  23. package/dist/lark-sdk/client.d.ts +8 -0
  24. package/dist/lark-sdk/client.js +32 -0
  25. package/dist/lark-sdk/index.d.ts +1 -0
  26. package/dist/lark-sdk/index.js +1 -0
  27. package/dist/lark-sender.d.ts +114 -0
  28. package/dist/lark-sender.js +323 -0
  29. package/dist/lark-session-store.d.ts +13 -0
  30. package/dist/lark-session-store.js +14 -0
  31. package/dist/lark-settings-card.d.ts +55 -0
  32. package/dist/lark-settings-card.js +158 -0
  33. package/dist/lark-setup.d.ts +17 -0
  34. package/dist/lark-setup.js +86 -0
  35. package/dist/lark.d.ts +500 -0
  36. package/dist/lark.js +2010 -0
  37. package/dist/runtime.d.ts +16 -0
  38. package/dist/runtime.js +157 -0
  39. package/dist/runtime.mjs +130863 -0
  40. package/package.json +35 -0
  41. package/rynx-plugin.json +42 -0
@@ -0,0 +1,347 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { watchFile, unwatchFile } from "node:fs";
3
+ import { open, readdir, stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ const NEWLINE = 0x0a;
7
+ export function resolveCodexSessionsRoot() {
8
+ return path.join(homedir(), ".codex", "sessions");
9
+ }
10
+ function defaultDeps(pollMs) {
11
+ return {
12
+ sessionsRoot: resolveCodexSessionsRoot(),
13
+ async readdir(dir) {
14
+ const entries = await readdir(dir, { withFileTypes: true });
15
+ return entries.map((entry) => ({
16
+ name: entry.name,
17
+ isDirectory: entry.isDirectory(),
18
+ isFile: entry.isFile(),
19
+ }));
20
+ },
21
+ async fileSize(filePath) {
22
+ return (await stat(filePath)).size;
23
+ },
24
+ async readRange(filePath, start, end) {
25
+ const length = Math.max(0, end - start);
26
+ if (length === 0) {
27
+ return Buffer.alloc(0);
28
+ }
29
+ const handle = await open(filePath, "r");
30
+ try {
31
+ const buffer = Buffer.alloc(length);
32
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
33
+ return buffer.subarray(0, bytesRead);
34
+ }
35
+ finally {
36
+ await handle.close();
37
+ }
38
+ },
39
+ watch(filePath, onChange) {
40
+ const listener = (curr, prev) => {
41
+ if (curr.size !== prev.size || curr.mtimeMs !== prev.mtimeMs) {
42
+ onChange();
43
+ }
44
+ };
45
+ watchFile(filePath, { interval: pollMs }, listener);
46
+ return () => unwatchFile(filePath, listener);
47
+ },
48
+ };
49
+ }
50
+ export class CodexRolloutWatcher {
51
+ onEvent;
52
+ deps;
53
+ logger;
54
+ sessions = new Map();
55
+ constructor(onEvent, options = {}) {
56
+ this.onEvent = onEvent;
57
+ const base = defaultDeps(options.pollMs ?? 1000);
58
+ this.deps = { ...base, ...options.deps };
59
+ this.logger = options.logger;
60
+ }
61
+ /**
62
+ * Begin tracking a Codex thread. Locates its rollout file and baselines the
63
+ * cursor to the current EOF (only NEW activity is mirrored — no replay). If
64
+ * the file doesn't exist yet, the id stays pending and {@link relocatePending}
65
+ * will pick it up later.
66
+ */
67
+ async register(codexSessionId) {
68
+ if (this.sessions.has(codexSessionId)) {
69
+ return;
70
+ }
71
+ const state = {
72
+ codexSessionId,
73
+ filePath: null,
74
+ cursor: 0,
75
+ pending: Buffer.alloc(0),
76
+ unwatch: null,
77
+ tail: Promise.resolve(),
78
+ };
79
+ this.sessions.set(codexSessionId, state);
80
+ await this.locate(state);
81
+ }
82
+ unregister(codexSessionId) {
83
+ const state = this.sessions.get(codexSessionId);
84
+ if (!state) {
85
+ return;
86
+ }
87
+ state.unwatch?.();
88
+ this.sessions.delete(codexSessionId);
89
+ }
90
+ /** Try to locate rollout files for any still-pending ids (called by the
91
+ * mirror's reconcile loop to pick up files created after registration). */
92
+ async relocatePending() {
93
+ await Promise.all([...this.sessions.values()]
94
+ .filter((state) => !state.filePath)
95
+ .map((state) => this.locate(state)));
96
+ }
97
+ /**
98
+ * Advance the cursor to current EOF without emitting events — used to "claim"
99
+ * lines the bridge itself wrote during a bridge-driven turn so the watcher
100
+ * doesn't re-mirror them.
101
+ */
102
+ async claimToEof(codexSessionId) {
103
+ const state = this.sessions.get(codexSessionId);
104
+ if (!state?.filePath) {
105
+ return;
106
+ }
107
+ try {
108
+ state.cursor = await this.deps.fileSize(state.filePath);
109
+ state.pending = Buffer.alloc(0);
110
+ }
111
+ catch (error) {
112
+ this.log("rollout.claim_failed", codexSessionId, error);
113
+ }
114
+ }
115
+ /** Test seam: feed appended text directly, bypassing the filesystem. */
116
+ ingestAppendedText(codexSessionId, text) {
117
+ const state = this.sessions.get(codexSessionId);
118
+ if (!state) {
119
+ return;
120
+ }
121
+ this.processChunk(state, Buffer.from(text, "utf8"));
122
+ }
123
+ stop() {
124
+ for (const state of this.sessions.values()) {
125
+ state.unwatch?.();
126
+ }
127
+ this.sessions.clear();
128
+ }
129
+ async locate(state) {
130
+ let filePath;
131
+ try {
132
+ filePath = await this.findRolloutFile(state.codexSessionId);
133
+ }
134
+ catch (error) {
135
+ this.log("rollout.locate_failed", state.codexSessionId, error);
136
+ return;
137
+ }
138
+ if (!filePath || state.filePath) {
139
+ return;
140
+ }
141
+ state.filePath = filePath;
142
+ try {
143
+ state.cursor = await this.deps.fileSize(filePath);
144
+ }
145
+ catch {
146
+ state.cursor = 0;
147
+ }
148
+ state.unwatch = this.deps.watch(filePath, () => {
149
+ this.scheduleGrowth(state);
150
+ });
151
+ }
152
+ async findRolloutFile(codexSessionId) {
153
+ const suffix = `-${codexSessionId}.jsonl`;
154
+ const walk = async (dir) => {
155
+ let entries;
156
+ try {
157
+ entries = await this.deps.readdir(dir);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ // Files first (the match is usually a leaf in a date directory).
163
+ for (const entry of entries) {
164
+ if (entry.isFile && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
165
+ return path.join(dir, entry.name);
166
+ }
167
+ }
168
+ for (const entry of entries) {
169
+ if (entry.isDirectory) {
170
+ const found = await walk(path.join(dir, entry.name));
171
+ if (found) {
172
+ return found;
173
+ }
174
+ }
175
+ }
176
+ return null;
177
+ };
178
+ const roots = [this.deps.sessionsRoot, ...(this.deps.extraSessionsRoots ?? [])];
179
+ for (const root of roots) {
180
+ const found = await walk(root);
181
+ if (found) {
182
+ return found;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ scheduleGrowth(state) {
188
+ state.tail = state.tail
189
+ .catch(() => undefined)
190
+ .then(() => this.processGrowth(state));
191
+ }
192
+ async processGrowth(state) {
193
+ if (!state.filePath) {
194
+ return;
195
+ }
196
+ let size;
197
+ try {
198
+ size = await this.deps.fileSize(state.filePath);
199
+ }
200
+ catch (error) {
201
+ this.log("rollout.stat_failed", state.codexSessionId, error);
202
+ return;
203
+ }
204
+ if (size < state.cursor) {
205
+ // Truncated/rotated unexpectedly — re-baseline without emitting.
206
+ state.cursor = size;
207
+ state.pending = Buffer.alloc(0);
208
+ return;
209
+ }
210
+ if (size === state.cursor) {
211
+ return;
212
+ }
213
+ let chunk;
214
+ try {
215
+ chunk = await this.deps.readRange(state.filePath, state.cursor, size);
216
+ }
217
+ catch (error) {
218
+ this.log("rollout.read_failed", state.codexSessionId, error);
219
+ return;
220
+ }
221
+ state.cursor += chunk.length;
222
+ this.processChunk(state, chunk);
223
+ }
224
+ processChunk(state, chunk) {
225
+ const combined = state.pending.length ? Buffer.concat([state.pending, chunk]) : chunk;
226
+ const lastNewline = combined.lastIndexOf(NEWLINE);
227
+ if (lastNewline < 0) {
228
+ state.pending = combined;
229
+ return;
230
+ }
231
+ const complete = combined.subarray(0, lastNewline).toString("utf8");
232
+ state.pending = combined.subarray(lastNewline + 1);
233
+ for (const rawLine of complete.split("\n")) {
234
+ const line = rawLine.trim();
235
+ if (!line) {
236
+ continue;
237
+ }
238
+ const event = this.mapLine(state.codexSessionId, line);
239
+ if (event) {
240
+ this.onEvent(event);
241
+ }
242
+ }
243
+ }
244
+ mapLine(codexSessionId, line) {
245
+ let parsed;
246
+ try {
247
+ parsed = JSON.parse(line);
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ if (!parsed || typeof parsed !== "object") {
253
+ return null;
254
+ }
255
+ const ts = parsed.timestamp;
256
+ const payload = parsed.payload;
257
+ if (!payload || typeof payload !== "object") {
258
+ return null;
259
+ }
260
+ const payloadType = typeof payload.type === "string" ? payload.type : undefined;
261
+ if (parsed.type === "event_msg") {
262
+ switch (payloadType) {
263
+ case "user_message": {
264
+ const text = typeof payload.message === "string" ? payload.message.trim() : "";
265
+ return text ? { kind: "userMessage", codexSessionId, text, ts } : null;
266
+ }
267
+ case "agent_message": {
268
+ const text = typeof payload.message === "string" ? payload.message.trim() : "";
269
+ if (!text) {
270
+ return null;
271
+ }
272
+ return payload.phase === "final_answer"
273
+ ? { kind: "assistantFinal", codexSessionId, text, ts }
274
+ : { kind: "assistantCommentary", codexSessionId, text, ts };
275
+ }
276
+ case "task_complete":
277
+ return { kind: "taskComplete", codexSessionId, ts };
278
+ default:
279
+ return null;
280
+ }
281
+ }
282
+ if (parsed.type === "response_item") {
283
+ switch (payloadType) {
284
+ case "function_call": {
285
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
286
+ const name = typeof payload.name === "string" ? payload.name : "tool";
287
+ return {
288
+ kind: "toolStart",
289
+ codexSessionId,
290
+ callId,
291
+ name,
292
+ command: extractCommand(payload.arguments),
293
+ ts,
294
+ };
295
+ }
296
+ case "function_call_output": {
297
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
298
+ return { kind: "toolEnd", codexSessionId, callId, output: stringifyOutput(payload.output), ts };
299
+ }
300
+ default:
301
+ return null;
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+ log(event, codexSessionId, error) {
307
+ this.logger?.log({
308
+ event,
309
+ codex_session_id: codexSessionId,
310
+ ...(error ? { error: error instanceof Error ? error.message : String(error) } : {}),
311
+ });
312
+ }
313
+ }
314
+ /** Pull a human-readable command from a `function_call.arguments` JSON string. */
315
+ function extractCommand(args) {
316
+ if (typeof args !== "string") {
317
+ return undefined;
318
+ }
319
+ try {
320
+ const parsed = JSON.parse(args);
321
+ const candidate = parsed.cmd ?? parsed.command;
322
+ if (typeof candidate === "string") {
323
+ return candidate;
324
+ }
325
+ if (Array.isArray(candidate)) {
326
+ return candidate.filter((part) => typeof part === "string").join(" ");
327
+ }
328
+ }
329
+ catch {
330
+ // Not JSON — ignore.
331
+ }
332
+ return undefined;
333
+ }
334
+ function stringifyOutput(output) {
335
+ if (output == null) {
336
+ return undefined;
337
+ }
338
+ if (typeof output === "string") {
339
+ return output;
340
+ }
341
+ try {
342
+ return JSON.stringify(output);
343
+ }
344
+ catch {
345
+ return undefined;
346
+ }
347
+ }
@@ -0,0 +1,82 @@
1
+ import { type AgentCapabilities, type AgentRuntimeId, type CapabilityResult, type ConversationRuntime, type ConversationStreamRequest, type ModelListResponse, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
2
+ import type { PluginAgentSummary, PluginHostClient, PluginResolvedSessionExecution, PluginSessionExecutionSnapshot } from "@rynx-ai/plugin-sdk";
3
+ import { FileLarkActiveSessionStore } from "./lark-session-store.js";
4
+ type LarkConversationRuntime = Pick<ConversationRuntime, "runLive">;
5
+ /** Slow consumers fail the run instead of retaining an unbounded event stream. */
6
+ export declare const LARK_RUN_EVENT_QUEUE_MAX_ITEMS = 256;
7
+ export declare const LARK_RUN_EVENT_QUEUE_MAX_BYTES: number;
8
+ /**
9
+ * ConversationRuntime-shaped adapter backed exclusively by capability-checked
10
+ * Host RPC. The plugin never receives the daemon's runtime, abort controller,
11
+ * steer controller, or session bus.
12
+ */
13
+ export declare class HostConversationRuntime implements LarkConversationRuntime {
14
+ private readonly host;
15
+ constructor(host: PluginHostClient);
16
+ runLive(input: ConversationStreamRequest): Promise<{
17
+ provider: "codex" | "traex" | "claude";
18
+ threadId: string;
19
+ model: string;
20
+ authSource: "codex_cli" | "traex_cli" | "claude_cli";
21
+ generator: AsyncGenerator<SessionEvent, any, any>;
22
+ }>;
23
+ }
24
+ /** Agent command adapter used by /models, /goal, and /fork. */
25
+ export declare class HostAgentCapabilities implements AgentCapabilities {
26
+ private readonly host;
27
+ constructor(host: PluginHostClient);
28
+ listModels(runtime?: AgentRuntimeId): Promise<ModelListResponse | null>;
29
+ getGoal(sessionId: string): Promise<CapabilityResult<ThreadGoal | null>>;
30
+ setGoal(sessionId: string, objective: string): Promise<CapabilityResult>;
31
+ clearGoal(sessionId: string): Promise<CapabilityResult>;
32
+ forkSession(fromSessionId: string, toSessionId: string): Promise<CapabilityResult>;
33
+ }
34
+ /** Narrow agent catalog + first-turn execution resolver backed by Host RPC. */
35
+ export declare class HostAgentCatalog {
36
+ private readonly host;
37
+ constructor(host: PluginHostClient);
38
+ list(): Promise<PluginAgentSummary[]>;
39
+ get(id: string): Promise<PluginAgentSummary | null>;
40
+ resolveExecution(input: {
41
+ defaultAgent?: string;
42
+ session: PluginSessionExecutionSnapshot;
43
+ }): Promise<PluginResolvedSessionExecution>;
44
+ }
45
+ /** Daemon-owned session allocator. operationId must identify one allocation attempt durably. */
46
+ export declare class HostSessionProvisioner {
47
+ private readonly host;
48
+ readonly instanceId: string;
49
+ constructor(host: PluginHostClient, instanceId: string);
50
+ provision(operationId: string): Promise<string>;
51
+ allocationOperationId(sessionKey: string, previousSessionId?: string): string;
52
+ forkOperationId(fromSessionId: string, newSessionKey: string): string;
53
+ setTitle(sessionId: string, title: string): Promise<void>;
54
+ }
55
+ /**
56
+ * File-backed Lark routing state whose every newly allocated machine session is
57
+ * minted by the daemon. Allocation operation ids are derived from the previous
58
+ * committed routing record, so a child crash between RPC and file commit replays
59
+ * the same host operation instead of creating a duplicate session.
60
+ */
61
+ export declare class HostProvisionedLarkSessionStore extends FileLarkActiveSessionStore {
62
+ private readonly operation;
63
+ private mutationTail;
64
+ constructor(options: {
65
+ filePath: string;
66
+ provisioner: HostSessionProvisioner;
67
+ defaultAgent?: string;
68
+ now?: () => string;
69
+ });
70
+ private readonly provisioner;
71
+ ensure(sessionKey: string): Promise<import("@rynx-ai/core").EnsureSessionResult>;
72
+ set(sessionKey: string, sessionId: string, cwd?: string): Promise<import("@rynx-ai/core").ConversationSessionRecord>;
73
+ rotate(sessionKey: string): Promise<import("@rynx-ai/core").RotateSessionResult>;
74
+ bindCwd(sessionKey: string, cwd: string): Promise<import("@rynx-ai/core").ConversationSessionRecord>;
75
+ applySettings(sessionKey: string, settings: Parameters<FileLarkActiveSessionStore["applySettings"]>[1]): Promise<import("@rynx-ai/core").ConversationSessionRecord>;
76
+ setAgent(sessionKey: string, agentName: string): Promise<import("@rynx-ai/core").ConversationSessionRecord>;
77
+ freeze(sessionKey: string, execution: Parameters<FileLarkActiveSessionStore["freeze"]>[1]): Promise<import("@rynx-ai/core").ConversationSessionRecord>;
78
+ private withAllocation;
79
+ private runMutation;
80
+ }
81
+ export declare function stableOperationId(kind: string, ...parts: string[]): string;
82
+ export type { LarkConversationRuntime };