@24klynx/cli 0.1.0

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 (38) hide show
  1. package/dist/break-cache-B716oddK.mjs +71 -0
  2. package/dist/break-cache-B716oddK.mjs.map +1 -0
  3. package/dist/bughunter-DeAizlBM.mjs +32 -0
  4. package/dist/bughunter-DeAizlBM.mjs.map +1 -0
  5. package/dist/clear-C1dFE5aD.mjs +24 -0
  6. package/dist/clear-C1dFE5aD.mjs.map +1 -0
  7. package/dist/config-D-xVXTXi.mjs +2 -0
  8. package/dist/config-Des0z-k9.mjs +147 -0
  9. package/dist/config-Des0z-k9.mjs.map +1 -0
  10. package/dist/context-BmZ8VEan.mjs +128 -0
  11. package/dist/context-BmZ8VEan.mjs.map +1 -0
  12. package/dist/context-viz-2ZZaTL2C.mjs +61 -0
  13. package/dist/context-viz-2ZZaTL2C.mjs.map +1 -0
  14. package/dist/env-CeeZcoDI.mjs +55 -0
  15. package/dist/env-CeeZcoDI.mjs.map +1 -0
  16. package/dist/git-branch-Dn1CP6An.mjs +96 -0
  17. package/dist/git-branch-Dn1CP6An.mjs.map +1 -0
  18. package/dist/headless-launcher-I8NWyD6k.mjs +171 -0
  19. package/dist/headless-launcher-I8NWyD6k.mjs.map +1 -0
  20. package/dist/index.d.mts +970 -0
  21. package/dist/index.d.mts.map +1 -0
  22. package/dist/index.mjs +3243 -0
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/memory-gnURjOnQ.mjs +199 -0
  25. package/dist/memory-gnURjOnQ.mjs.map +1 -0
  26. package/dist/privacy-B6Rm1Xck.mjs +114 -0
  27. package/dist/privacy-B6Rm1Xck.mjs.map +1 -0
  28. package/dist/process-lifecycle-Dg6n2QS-.mjs +784 -0
  29. package/dist/process-lifecycle-Dg6n2QS-.mjs.map +1 -0
  30. package/dist/sandbox-toggle-9akjTw3h.mjs +64 -0
  31. package/dist/sandbox-toggle-9akjTw3h.mjs.map +1 -0
  32. package/dist/stats-DjKezhTJ.mjs +73 -0
  33. package/dist/stats-DjKezhTJ.mjs.map +1 -0
  34. package/dist/status-B3Tw-Ef4.mjs +92 -0
  35. package/dist/status-B3Tw-Ef4.mjs.map +1 -0
  36. package/dist/upgrade-CREWRNeC.mjs +72 -0
  37. package/dist/upgrade-CREWRNeC.mjs.map +1 -0
  38. package/package.json +39 -0
@@ -0,0 +1,784 @@
1
+ import { n as loadConfig } from "./config-Des0z-k9.mjs";
2
+ import { migrate, openDatabase, resolvePaths } from "@lynx/core";
3
+ import { createSessionManager } from "@lynx/session";
4
+ import { createMcpAuthHandler, createMemoryWriteHandler, createSendMessageHandler, createToolRegistry, injectTaskManager, mcpAuthDescriptor, memoryWriteDescriptor, registerBuiltinTools, sendMessageDescriptor } from "@lynx/tools";
5
+ import { PredictiveLoader, createHookRegistry, createManifestRegistry, createPluginLoader } from "@lynx/plugins";
6
+ import { createMcpManager, createMemoryManager, createQueryEngine, createSkillRegistry, createSkillToolHandler, createTaskManager, getBaseSystemPrompt } from "@lynx/agent";
7
+ import { createDenialTracker, createRuleEngine, createRulesLoader, loadFromDisk } from "@lynx/permissions";
8
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { randomUUID } from "node:crypto";
12
+ import { createChannelRegistry, createFeishuAdapter } from "@lynx/channels";
13
+ //#region src/mcp-loader.ts
14
+ /**
15
+ * MCP configuration loader — reads MCP server configurations
16
+ * from Lynx settings files (3‑layer merge: global → project → local).
17
+ *
18
+ * Settings layers (later overrides earlier for same‑named servers):
19
+ * 1. ~/.lynx/settings.json — global
20
+ * 2. .lynx/settings.json — project (committed)
21
+ * 3. .lynx/settings.local.json — project local (gitignored)
22
+ */
23
+ /**
24
+ * Try to read and parse a JSON file. Returns undefined if the file
25
+ * does not exist or is malformed.
26
+ */
27
+ function tryReadJson(filePath) {
28
+ if (!existsSync(filePath)) return void 0;
29
+ try {
30
+ return JSON.parse(readFileSync(filePath, "utf-8"));
31
+ } catch {
32
+ return;
33
+ }
34
+ }
35
+ /**
36
+ * Find the project root by walking up from cwd looking for `.lynx/`.
37
+ * Returns undefined if no project root is found.
38
+ */
39
+ function findProjectRoot(startDir) {
40
+ let dir = startDir;
41
+ for (let i = 0; i < 50; i++) {
42
+ if (existsSync(join(dir, ".lynx"))) return dir;
43
+ const parent = join(dir, "..");
44
+ if (parent === dir) return void 0;
45
+ dir = parent;
46
+ }
47
+ }
48
+ /**
49
+ * Load MCP server configurations from all settings layers.
50
+ *
51
+ * Merge order: global → project → project‑local.
52
+ * Same‑named servers in later layers override earlier ones entirely.
53
+ */
54
+ function loadMcpConfigs(workspaceDir) {
55
+ const globalSettings = tryReadJson(resolvePaths().settingsFile);
56
+ const projectRoot = workspaceDir ? findProjectRoot(workspaceDir) : findProjectRoot(process.cwd());
57
+ const projectSettings = projectRoot ? tryReadJson(join(projectRoot, ".lynx", "settings.json")) : void 0;
58
+ const localSettings = projectRoot ? tryReadJson(join(projectRoot, ".lynx", "settings.local.json")) : void 0;
59
+ const merged = /* @__PURE__ */ new Map();
60
+ function addServers(servers, source) {
61
+ if (!servers) return;
62
+ for (const server of servers) merged.set(server.name, {
63
+ config: server,
64
+ source
65
+ });
66
+ }
67
+ addServers(globalSettings?.mcpServers, "global");
68
+ addServers(projectSettings?.mcpServers, "project");
69
+ addServers(localSettings?.mcpServers, "project-local");
70
+ return {
71
+ servers: Array.from(merged.values()).map((e) => e.config),
72
+ entries: merged
73
+ };
74
+ }
75
+ /**
76
+ * Convenience: load and validate MCP configs for bootstrap.
77
+ *
78
+ * Returns an array of valid McpServerConfig objects, filtering out
79
+ * entries that have neither a `command` nor a `url`.
80
+ */
81
+ function loadAndValidateMcpConfigs(workspaceDir) {
82
+ return loadMcpConfigs(workspaceDir).servers.filter((s) => {
83
+ const isValid = !!(s.command || s.url);
84
+ if (!isValid) {}
85
+ return isValid;
86
+ });
87
+ }
88
+ //#endregion
89
+ //#region src/bootstrap.ts
90
+ /** Timeout for auto‑denying a permission request when the user doesn't respond. */
91
+ const PERMISSION_TIMEOUT_MS = 6e4;
92
+ /**
93
+ * Create a permission bridge that mediates between the agent engine
94
+ * and the TUI's permission dialog.
95
+ *
96
+ * The bridge uses a Promise Map pattern:
97
+ * 1. Agent calls `requestPermission()` → Promise created + stored
98
+ * 2. TUI handler fires → shows permission dialog
99
+ * 3. User responds → `handleReply()` resolves the Promise
100
+ * 4. Agent resumes (or skips the tool)
101
+ *
102
+ * Safety levels "Safe" and "WorkspaceSafe" are auto‑allowed without
103
+ * showing the dialog.
104
+ */
105
+ function createPermissionBridge() {
106
+ const pending = /* @__PURE__ */ new Map();
107
+ let tuiHandler = null;
108
+ return {
109
+ async requestPermission(toolName, safety, description) {
110
+ if (safety === "Safe" || safety === "WorkspaceSafe") return true;
111
+ return new Promise((resolve) => {
112
+ const requestId = randomUUID();
113
+ const timer = setTimeout(() => {
114
+ pending.delete(requestId);
115
+ resolve(false);
116
+ }, PERMISSION_TIMEOUT_MS);
117
+ pending.set(requestId, {
118
+ resolve,
119
+ timer
120
+ });
121
+ if (tuiHandler) tuiHandler({
122
+ requestId,
123
+ toolName,
124
+ description,
125
+ safety
126
+ });
127
+ else {
128
+ clearTimeout(timer);
129
+ pending.delete(requestId);
130
+ resolve(false);
131
+ }
132
+ });
133
+ },
134
+ handleReply(requestId, approved) {
135
+ const entry = pending.get(requestId);
136
+ if (entry) {
137
+ clearTimeout(entry.timer);
138
+ pending.delete(requestId);
139
+ entry.resolve(approved);
140
+ }
141
+ },
142
+ setTuiHandler(handler) {
143
+ tuiHandler = handler;
144
+ }
145
+ };
146
+ }
147
+ /**
148
+ * Load all .md files from a directory as an array of file contents.
149
+ * Silently returns [] if the directory doesn't exist or is unreadable.
150
+ */
151
+ function loadTextFilesFromDir(dir) {
152
+ const facts = [];
153
+ let entries;
154
+ try {
155
+ entries = readdirSync(dir);
156
+ } catch {
157
+ return facts;
158
+ }
159
+ for (const entry of entries) {
160
+ if (!entry.endsWith(".md")) continue;
161
+ const fullPath = join(dir, entry);
162
+ try {
163
+ if (!statSync(fullPath).isFile()) continue;
164
+ facts.push(readFileSync(fullPath, "utf-8"));
165
+ } catch {}
166
+ }
167
+ return facts;
168
+ }
169
+ /**
170
+ * Bootstrap the entire Lynx application.
171
+ *
172
+ * This is the ONLY place where cross‑package wiring happens.
173
+ * Every other module only talks to its immediate dependencies
174
+ * through explicit factory‑injected interfaces.
175
+ */
176
+ function bootstrap(config) {
177
+ const dbPath = join(config.homeDir, "state.db");
178
+ const workspace = config.workspace ?? process.cwd();
179
+ const db = openDatabase({ dbPath });
180
+ migrate(db);
181
+ const sessionMgr = createSessionManager(db);
182
+ const toolRegistry = createToolRegistry();
183
+ const ruleEngine = createRuleEngine();
184
+ const rulesLoader = createRulesLoader(ruleEngine);
185
+ registerBuiltinTools(toolRegistry);
186
+ const skillRegistry = createSkillRegistry(config.skillsDir ?? join(config.homeDir, "..", "packages", "lynx-agent", "skills"));
187
+ const userSkillsDir = join(homedir(), ".lynx", "skills");
188
+ skillRegistry.addDirectory(userSkillsDir);
189
+ const projectSkillsDir = join(workspace, ".claude", "skills");
190
+ skillRegistry.addDirectory(projectSkillsDir);
191
+ const skills = skillRegistry.list();
192
+ toolRegistry.register({
193
+ name: "Skill",
194
+ description: "加载技能完整指令。支持 load(加载技能内容)、list(列出所有技能)、search(搜索技能)、reload(重新加载技能目录)四种操作。",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ action: {
199
+ type: "string",
200
+ enum: [
201
+ "load",
202
+ "list",
203
+ "search",
204
+ "reload"
205
+ ],
206
+ description: "操作类型:load 加载技能内容、list 列出所有技能、search 搜索技能、reload 重新加载"
207
+ },
208
+ skill: {
209
+ type: "string",
210
+ description: "要加载的技能名称(load 操作时使用)"
211
+ },
212
+ args: {
213
+ type: "string",
214
+ description: "传递给技能的可选参数"
215
+ },
216
+ query: {
217
+ type: "string",
218
+ description: "搜索关键词(search 操作时使用)"
219
+ }
220
+ }
221
+ },
222
+ kind: "ReadOnly",
223
+ safety: "Safe",
224
+ availability: { type: "always" },
225
+ executor: "Skill",
226
+ owner: "core"
227
+ }, createSkillToolHandler(skillRegistry));
228
+ const mcpManager = createMcpManager();
229
+ const settingsMcpServers = loadAndValidateMcpConfigs();
230
+ const programmaticServers = config.mcpServers ?? [];
231
+ const settingsOnly = settingsMcpServers.filter((s) => !programmaticServers.some((p) => p.name === s.name));
232
+ const allMcpServers = [...programmaticServers, ...settingsOnly];
233
+ for (const serverConfig of allMcpServers) mcpManager.connect(serverConfig).catch(() => {});
234
+ const channelRegistry = createChannelRegistry();
235
+ if (config.feishu?.appId && config.feishu?.appSecret) {
236
+ const feishuAdapter = createFeishuAdapter(config.feishu);
237
+ channelRegistry.register(feishuAdapter);
238
+ }
239
+ const toolHandlers = /* @__PURE__ */ new Map();
240
+ const allTools = toolRegistry.listAll();
241
+ for (const desc of allTools) try {
242
+ const handler = toolRegistry.resolveExecutor(desc);
243
+ toolHandlers.set(desc.name, handler);
244
+ } catch {}
245
+ const mcpDispatcher = { async handle(invocation, _signal) {
246
+ const result = await mcpManager.callTool(invocation.toolName, invocation.payload);
247
+ return {
248
+ content: result.content,
249
+ success: !result.isError
250
+ };
251
+ } };
252
+ for (const mcpTool of mcpManager.getAllTools()) {
253
+ if (toolHandlers.has(mcpTool.name)) {
254
+ process.stderr.write(`[lynx] 警告:MCP 工具 "${mcpTool.name}" 与现有工具冲突 — 已跳过\n`);
255
+ continue;
256
+ }
257
+ toolHandlers.set(mcpTool.name, mcpDispatcher);
258
+ }
259
+ new Set(mcpManager.getAllTools().map((t) => t.name));
260
+ const allToolsWithMcp = [...allTools, ...mcpManager.getAllTools().filter((t) => !allTools.some((b) => b.name === t.name))];
261
+ const manifestRegistry = createManifestRegistry();
262
+ const hookRegistry = createHookRegistry();
263
+ const pluginRegistry = {
264
+ manifestRegistry,
265
+ loader: createPluginLoader(),
266
+ hooks: hookRegistry
267
+ };
268
+ const predictiveLoader = new PredictiveLoader(async (pluginId) => {
269
+ return import(pluginId);
270
+ });
271
+ loadFromDisk(rulesLoader, workspace);
272
+ const denialTracker = createDenialTracker();
273
+ const permissionBridge = createPermissionBridge();
274
+ const userMemoryDir = join(homedir(), ".lynx", "memory");
275
+ const projectMemoryDir = join(workspace, ".claude", "memory");
276
+ const memoryManager = createMemoryManager(userMemoryDir);
277
+ const memoryFacts = [...memoryManager.list().map((e) => memoryManager.get(e.name)?.content).filter(Boolean), ...loadTextFilesFromDir(projectMemoryDir)];
278
+ const memoryWriteHandler = createMemoryWriteHandler(memoryManager);
279
+ toolRegistry.register(memoryWriteDescriptor, memoryWriteHandler);
280
+ toolHandlers.set(memoryWriteDescriptor.name, memoryWriteHandler);
281
+ allTools.push(memoryWriteDescriptor);
282
+ const mcpAuthHandler = createMcpAuthHandler({ reconnect: async (serverName) => {
283
+ return { status: (await mcpManager.reconnect(serverName)).status };
284
+ } });
285
+ toolRegistry.register(mcpAuthDescriptor, mcpAuthHandler);
286
+ toolHandlers.set(mcpAuthDescriptor.name, mcpAuthHandler);
287
+ allTools.push(mcpAuthDescriptor);
288
+ const sendMessageHandler = createSendMessageHandler({ async send(channel, content, recipients) {
289
+ const adapter = channelRegistry.get(channel);
290
+ if (!adapter) throw new Error(`未注册的消息通道:${channel}`);
291
+ const message = {
292
+ id: randomUUID(),
293
+ role: "assistant",
294
+ content: [{
295
+ type: "text",
296
+ text: content
297
+ }],
298
+ timestamp: Date.now(),
299
+ turnIndex: 0
300
+ };
301
+ return { messageId: await adapter.sendMessage(message) };
302
+ } });
303
+ toolRegistry.register(sendMessageDescriptor, sendMessageHandler);
304
+ toolHandlers.set(sendMessageDescriptor.name, sendMessageHandler);
305
+ allTools.push(sendMessageDescriptor);
306
+ const globalLynxMd = join(homedir(), ".lynx", "LYNX.md");
307
+ const globalInstructions = [];
308
+ try {
309
+ if (existsSync(globalLynxMd) && statSync(globalLynxMd).isFile()) globalInstructions.push(`# LYNX.md — 全局用户指令\n${readFileSync(globalLynxMd, "utf-8")}`);
310
+ } catch {}
311
+ const userRulesDir = join(homedir(), ".lynx", "rules");
312
+ const projectRulesDir = join(workspace, ".claude", "rules");
313
+ const rules = [
314
+ ...globalInstructions,
315
+ ...loadTextFilesFromDir(userRulesDir),
316
+ ...loadTextFilesFromDir(projectRulesDir)
317
+ ];
318
+ const agentConfig = {
319
+ provider: config.provider,
320
+ model: config.model,
321
+ systemPrompt: getBaseSystemPrompt(),
322
+ maxTokens: 8192,
323
+ maxCompactionFailures: 3,
324
+ budget: {
325
+ maxTokens: 2e5,
326
+ maxUsd: 10,
327
+ maxTurns: 100
328
+ }
329
+ };
330
+ const engine = createQueryEngine({
331
+ config: agentConfig,
332
+ provider: config.provider,
333
+ toolHandlers,
334
+ allTools: allToolsWithMcp,
335
+ skills,
336
+ memoryFacts,
337
+ rules,
338
+ checkPermission: async (toolName, safety, description) => {
339
+ if (denialTracker.isTripped()) return false;
340
+ const safetyLevel = safety;
341
+ const approved = await permissionBridge.requestPermission(toolName, safetyLevel, description);
342
+ if (safetyLevel === "RequiresApproval" || safetyLevel === "Dangerous") if (approved) denialTracker.recordApproval();
343
+ else denialTracker.recordDenial();
344
+ return approved;
345
+ }
346
+ });
347
+ const taskManager = createTaskManager();
348
+ injectTaskManager(taskManager);
349
+ return {
350
+ db,
351
+ sessionMgr,
352
+ toolRegistry,
353
+ pluginRegistry,
354
+ ruleEngine,
355
+ engine,
356
+ provider: config.provider,
357
+ agentConfig,
358
+ permissionBridge,
359
+ skillRegistry,
360
+ skills,
361
+ mcpManager,
362
+ predictiveLoader,
363
+ channelRegistry,
364
+ memoryManager,
365
+ memoryFacts,
366
+ rules,
367
+ destroy() {
368
+ taskManager.destroy();
369
+ channelRegistry.destroyAll();
370
+ mcpManager.destroy();
371
+ engine.destroy();
372
+ sessionMgr.destroy();
373
+ db.close();
374
+ }
375
+ };
376
+ }
377
+ //#endregion
378
+ //#region src/startup.ts
379
+ /**
380
+ * Run Phase 1 — blocking startup that must finish before
381
+ * the user sees anything useful.
382
+ *
383
+ * Returns a database handle the rest of the app can use.
384
+ */
385
+ function runPhase1() {
386
+ const started = Date.now();
387
+ const paths = resolvePaths();
388
+ const db = openDatabase({ dbPath: paths.stateDb });
389
+ migrate(db);
390
+ return {
391
+ paths,
392
+ db,
393
+ elapsedMs: Date.now() - started
394
+ };
395
+ }
396
+ /**
397
+ * Run Phase 2 — background work that executes after the TUI is rendered.
398
+ *
399
+ * Tasks run sequentially with a setImmediate gap between each so the
400
+ * event loop stays responsive. Each task is independently try‑catched
401
+ * so a single failure doesn't block the rest.
402
+ *
403
+ * Returns the elapsed time in ms.
404
+ */
405
+ async function runPhase2(result) {
406
+ return runPhase2Tasks(null, result.paths);
407
+ }
408
+ /**
409
+ * Run Phase 2 with the full AppContext (preferred path).
410
+ *
411
+ * When called from the TUI launcher, the AppContext provides access
412
+ * to all initialized services (plugin registry, MCP manager, etc.).
413
+ *
414
+ * Tasks:
415
+ * 1. scanExtensions — discover plugins in the extensions directory
416
+ * 2. preloadSkills — index skills for autocomplete
417
+ * 3. connectMcpServers — establish MCP connections
418
+ * 4. loadMemory — load memory files into the agent context
419
+ */
420
+ async function runPhase2WithContext(ctx, paths) {
421
+ return runPhase2Tasks(ctx, paths);
422
+ }
423
+ function createSilentLogger() {
424
+ return {
425
+ info: () => {},
426
+ warn: (msg) => process.stderr.write(`[phase2] ${msg}\n`)
427
+ };
428
+ }
429
+ async function runPhase2Tasks(ctx, paths) {
430
+ const logger = createSilentLogger();
431
+ const results = [];
432
+ const tasks = [
433
+ async () => {
434
+ await scanExtensions(ctx, paths, logger);
435
+ },
436
+ async () => {
437
+ await preloadSkills(ctx, paths, logger);
438
+ },
439
+ async () => {
440
+ await connectMcpServers(ctx, logger);
441
+ },
442
+ async () => {
443
+ await loadMemory(ctx, paths, logger);
444
+ },
445
+ async () => {
446
+ await initChannels(ctx, logger);
447
+ }
448
+ ];
449
+ for (const task of tasks) {
450
+ await new Promise((resolve) => setImmediate(resolve));
451
+ const started = Date.now();
452
+ const name = task.name || "unknown";
453
+ try {
454
+ await task();
455
+ results.push({
456
+ name,
457
+ ok: true,
458
+ elapsedMs: Date.now() - started
459
+ });
460
+ } catch (err) {
461
+ const message = err instanceof Error ? err.message : String(err);
462
+ logger.warn(`${name} failed: ${message}`);
463
+ results.push({
464
+ name,
465
+ ok: false,
466
+ error: message,
467
+ elapsedMs: Date.now() - started
468
+ });
469
+ }
470
+ }
471
+ if (results.some((r) => !r.ok)) {
472
+ const failures = results.filter((r) => !r.ok).map((r) => r.name).join(", ");
473
+ logger.warn(`Phase 2 completed with failures: ${failures}`);
474
+ } else logger.info(`Phase 2 completed: ${results.length} tasks OK`);
475
+ return results;
476
+ }
477
+ /**
478
+ * Scan the extensions directory for plugins, register manifests,
479
+ * and execute plugin code via the PluginLoader.
480
+ *
481
+ * Looks in:
482
+ * 1. <lynx-home>/extensions/ (user extensions)
483
+ * 2. <project>/.lynx/extensions/ (project extensions)
484
+ * 3. <lynx-install>/extensions/ (built‑in extensions)
485
+ */
486
+ async function scanExtensions(ctx, paths, logger) {
487
+ if (!ctx) return;
488
+ const { readdirSync, statSync } = await import("node:fs");
489
+ const { join } = await import("node:path");
490
+ const scanDirs = [join(paths.home, "extensions"), join(process.cwd(), ".lynx", "extensions")];
491
+ for (const dir of scanDirs) {
492
+ let entries;
493
+ try {
494
+ entries = readdirSync(dir);
495
+ } catch {
496
+ continue;
497
+ }
498
+ for (const entry of entries) {
499
+ const fullPath = join(dir, entry);
500
+ try {
501
+ if (!statSync(fullPath).isDirectory()) continue;
502
+ } catch {
503
+ continue;
504
+ }
505
+ const manifestPath = join(fullPath, "manifest.json");
506
+ try {
507
+ statSync(manifestPath);
508
+ } catch {
509
+ continue;
510
+ }
511
+ try {
512
+ const discovered = ctx.pluginRegistry.manifestRegistry.scan([fullPath]);
513
+ for (const manifestEntry of discovered) {
514
+ const pluginCtx = {
515
+ pluginId: manifestEntry.manifest.name,
516
+ logger: {
517
+ info: (msg) => logger.info(`[plugin:${manifestEntry.manifest.name}] ${msg}`),
518
+ warn: (msg) => logger.warn(`[plugin:${manifestEntry.manifest.name}] ${msg}`),
519
+ error: (msg) => logger.warn(`[plugin:${manifestEntry.manifest.name}] ${msg}`)
520
+ },
521
+ config: {},
522
+ storage: createPluginStorage()
523
+ };
524
+ ctx.pluginRegistry.loader.load(manifestEntry, pluginCtx).catch((err) => {
525
+ logger.warn(`Plugin "${manifestEntry.manifest.name}" failed to load: ${String(err)}`);
526
+ });
527
+ }
528
+ } catch {}
529
+ }
530
+ }
531
+ }
532
+ /**
533
+ * Create a simple in‑memory KV store for plugin storage.
534
+ *
535
+ * In Phase 5 this will be backed by the SQLite database
536
+ * for persistence across sessions.
537
+ */
538
+ function createPluginStorage() {
539
+ const store = /* @__PURE__ */ new Map();
540
+ return {
541
+ get(key) {
542
+ return store.get(key);
543
+ },
544
+ set(key, value) {
545
+ store.set(key, value);
546
+ },
547
+ delete(key) {
548
+ store.delete(key);
549
+ },
550
+ clear() {
551
+ store.clear();
552
+ }
553
+ };
554
+ }
555
+ /**
556
+ * Preload skill definitions for autocomplete and quick access.
557
+ *
558
+ * Skills are already loaded during bootstrap (Phase 1). This Phase 2 task
559
+ * enqueues plugins for predictive warming through the PredictiveLoader,
560
+ * reducing first‑use latency for plugins that are likely to be needed.
561
+ */
562
+ async function preloadSkills(ctx, _paths, _logger) {
563
+ if (!ctx) return;
564
+ const loader = ctx.predictiveLoader;
565
+ loader.onInputPrefix("/");
566
+ loader.onInputPrefix("@");
567
+ await loader.processQueue();
568
+ }
569
+ /**
570
+ * Connect to configured MCP servers.
571
+ *
572
+ * MCP connections are established during bootstrap (Phase 1) via
573
+ * McpManager.connect() for all configured servers. Connection
574
+ * failures are tracked via McpConnection.status.
575
+ *
576
+ * This Phase 2 task exists as a hook point for re‑connection or
577
+ * health check logic in future phases.
578
+ */
579
+ async function connectMcpServers(_ctx, _logger) {}
580
+ /**
581
+ * Load memory files into the agent's context.
582
+ *
583
+ * Memory is loaded during bootstrap (Phase 1) from:
584
+ * 1. ~/.lynx/memory/
585
+ * 2. <workspace>/.claude/memory/
586
+ *
587
+ * Content flows through AppContext.memoryFacts → EngineDeps →
588
+ * LoopDeps → assembleSystemPrompt ("Memory" section).
589
+ *
590
+ * This Phase 2 task exists as a hook point for runtime memory
591
+ * reload (e.g. after file changes).
592
+ */
593
+ async function loadMemory(_ctx, paths, _logger) {}
594
+ /**
595
+ * Initialize registered channel adapters (飞书 etc.).
596
+ *
597
+ * Channels are registered during bootstrap (Phase 1) but their
598
+ * init() (auth, WS connect) is deferred to Phase 2 so the TUI
599
+ * is already visible before network I/O starts.
600
+ */
601
+ async function initChannels(ctx, logger) {
602
+ if (!ctx) return;
603
+ const channelIds = ctx.channelRegistry.list();
604
+ if (channelIds.length === 0) {
605
+ logger.info("No channel adapters registered — skipping channel init");
606
+ return;
607
+ }
608
+ logger.info(`Initializing channels: ${channelIds.join(", ")}`);
609
+ await ctx.channelRegistry.initAll();
610
+ logger.info(`Channels initialized: ${channelIds.join(", ")}`);
611
+ }
612
+ //#endregion
613
+ //#region src/terminal-mode.ts
614
+ /**
615
+ * Terminal mode management — alt buffer, mouse tracking, DEC sync.
616
+ *
617
+ * Provides functions to enter/exit the alternate screen buffer,
618
+ * enable/disable mouse tracking, and wrap output with DEC
619
+ * Synchronized Update markers for flicker‑free rendering.
620
+ *
621
+ * All escape sequences are no‑ops on unsupported terminals
622
+ * (graceful degradation).
623
+ */
624
+ /** Enter alternate screen buffer. */
625
+ const ALT_ENTER = "\x1B[?1049h";
626
+ /** Exit alternate screen buffer. */
627
+ const ALT_EXIT = "\x1B[?1049l";
628
+ /** Begin DEC Synchronized Update — subsequent output is buffered. */
629
+ const BSU = "\x1B[?2026h";
630
+ /** End DEC Synchronized Update — flush buffered output atomically. */
631
+ const ESU = "\x1B[?2026l";
632
+ /** Enter fullscreen mode: alt buffer only (no mouse tracking). */
633
+ function enterFullscreen() {
634
+ process.stdout.write(ALT_ENTER);
635
+ }
636
+ /** Exit fullscreen mode: restore main buffer. */
637
+ function exitFullscreen() {
638
+ process.stdout.write(ALT_EXIT);
639
+ }
640
+ /**
641
+ * Wrap a synchronous callback with DEC Synchronized Update markers.
642
+ *
643
+ * All output written to stdout during the callback is buffered by the
644
+ * terminal and rendered atomically, eliminating flicker during re‑renders.
645
+ *
646
+ * Does NOT nest — calling beginSync inside an active sync region
647
+ * is a no‑op (tracked via module‑level flag).
648
+ */
649
+ let syncActive = false;
650
+ function beginSync() {
651
+ if (syncActive) return;
652
+ syncActive = true;
653
+ process.stdout.write(BSU);
654
+ }
655
+ function endSync() {
656
+ if (!syncActive) return;
657
+ syncActive = false;
658
+ process.stdout.write(ESU);
659
+ }
660
+ /**
661
+ * Execute a callback within a DEC synchronized update region.
662
+ * Exceptions propagate; sync is always ended.
663
+ */
664
+ function withSync(fn) {
665
+ beginSync();
666
+ try {
667
+ return fn();
668
+ } finally {
669
+ endSync();
670
+ }
671
+ }
672
+ //#endregion
673
+ //#region src/process-lifecycle.ts
674
+ let hardExitTimer = null;
675
+ let installed = false;
676
+ /** Maximum time (ms) allowed for graceful shutdown before force exit. */
677
+ const FORCE_EXIT_MS = 2e3;
678
+ /**
679
+ * Install process lifecycle handlers.
680
+ *
681
+ * Only call once. Subsequent calls are no‑ops.
682
+ * Handles SIGINT (3‑layer abort), SIGTERM (graceful shutdown),
683
+ * SIGHUP (graceful shutdown), and terminal loss.
684
+ */
685
+ function installProcessLifecycle(config) {
686
+ if (installed) return;
687
+ installed = true;
688
+ const { ctx, onAbortLl, onAbortTool, incrementAbortLayer, isStreaming } = config;
689
+ const onSigint = () => {
690
+ const layer = incrementAbortLayer();
691
+ if (layer >= 3) {
692
+ exitFullscreen();
693
+ ctx.destroy();
694
+ process.exit(1);
695
+ }
696
+ if (!isStreaming()) {
697
+ exitFullscreen();
698
+ ctx.destroy();
699
+ process.exit(0);
700
+ }
701
+ if (layer === 1) onAbortLl();
702
+ else if (layer === 2) onAbortTool();
703
+ };
704
+ process.on("SIGINT", onSigint);
705
+ const gracefulShutdown = () => {
706
+ startForceExitTimer();
707
+ try {
708
+ exitFullscreen();
709
+ } catch {}
710
+ try {
711
+ ctx.destroy();
712
+ } catch {}
713
+ process.exit(0);
714
+ };
715
+ process.on("SIGTERM", gracefulShutdown);
716
+ process.on("SIGHUP", () => {
717
+ try {
718
+ const newCfg = loadConfig();
719
+ if (typeof newCfg.model === "string" && newCfg.model !== ctx.agentConfig.model) {
720
+ ctx.agentConfig.model = newCfg.model;
721
+ process.stderr.write(`[lynx] SIGHUP:模型已切换 → ${newCfg.model}\n`);
722
+ }
723
+ if (typeof newCfg.theme === "string") process.stderr.write(`[lynx] SIGHUP:主题已切换 → ${newCfg.theme}\n`);
724
+ process.stderr.write("[lynx] SIGHUP:配置已重新加载\n");
725
+ } catch (reloadErr) {
726
+ process.stderr.write(`[lynx] SIGHUP 重新加载失败:${reloadErr instanceof Error ? reloadErr.message : String(reloadErr)} — 回退到关闭流程\n`);
727
+ gracefulShutdown();
728
+ }
729
+ });
730
+ const onTerminalLost = () => {
731
+ startForceExitTimer();
732
+ try {
733
+ ctx.destroy();
734
+ } catch {}
735
+ process.exit(0);
736
+ };
737
+ process.stdin.on("end", onTerminalLost);
738
+ process.stdin.on("close", onTerminalLost);
739
+ process.stdout.on("close", onTerminalLost);
740
+ process.on("unhandledRejection", (reason) => {
741
+ process.stderr.write(`[lynx] 未处理的 Promise 拒绝:${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}\n`);
742
+ startForceExitTimer();
743
+ exitFullscreen();
744
+ ctx.destroy();
745
+ process.exit(1);
746
+ });
747
+ process.on("uncaughtException", (err) => {
748
+ process.stderr.write(`[lynx] 未捕获的异常:${err.stack ?? err.message}\n`);
749
+ startForceExitTimer();
750
+ exitFullscreen();
751
+ ctx.destroy();
752
+ process.exit(1);
753
+ });
754
+ }
755
+ /**
756
+ * Register a cleanup handler that runs during graceful shutdown.
757
+ * Replaces any previously registered handler.
758
+ */
759
+ function onCleanup(handler) {}
760
+ /**
761
+ * Remove all process lifecycle handlers (for testing).
762
+ */
763
+ function uninstallProcessLifecycle() {
764
+ installed = false;
765
+ process.removeAllListeners("SIGINT");
766
+ process.removeAllListeners("SIGTERM");
767
+ process.removeAllListeners("SIGHUP");
768
+ if (hardExitTimer) {
769
+ clearTimeout(hardExitTimer);
770
+ hardExitTimer = null;
771
+ }
772
+ }
773
+ function startForceExitTimer() {
774
+ if (hardExitTimer) return;
775
+ hardExitTimer = setTimeout(() => {
776
+ process.stderr.write("[lynx] 超时后强制退出\n");
777
+ process.exit(1);
778
+ }, FORCE_EXIT_MS);
779
+ hardExitTimer.unref();
780
+ }
781
+ //#endregion
782
+ export { endSync as a, withSync as c, runPhase2WithContext as d, bootstrap as f, beginSync as i, runPhase1 as l, onCleanup as n, enterFullscreen as o, uninstallProcessLifecycle as r, exitFullscreen as s, installProcessLifecycle as t, runPhase2 as u };
783
+
784
+ //# sourceMappingURL=process-lifecycle-Dg6n2QS-.mjs.map