@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,96 @@
1
+ //#region src/commands/git-branch.ts
2
+ /**
3
+ * 处理 /branch 命令,返回一段中文指令引导模型完成分支操作。
4
+ */
5
+ function handleBranchCommand(args) {
6
+ const { action = "list", name, base } = args;
7
+ switch (action) {
8
+ case "list": return { instruction: buildListInstruction() };
9
+ case "create": return { instruction: buildCreateInstruction(name, base) };
10
+ case "switch": return { instruction: buildSwitchInstruction(name) };
11
+ case "delete": return { instruction: buildDeleteInstruction(name) };
12
+ default: return { instruction: `未知操作:${action}。可选操作:list、create、switch、delete` };
13
+ }
14
+ }
15
+ /** 构建列出分支的指令。 */
16
+ function buildListInstruction() {
17
+ return [
18
+ "# Git 分支列表",
19
+ "",
20
+ "请执行以下步骤列出所有分支:",
21
+ "",
22
+ "1. 运行 `git branch -a` 列出所有本地和远程分支",
23
+ "2. 运行 `git branch --show-current` 获取当前分支名",
24
+ "3. 格式化输出:",
25
+ " - 当前分支用 `*` 或特殊标记高亮显示",
26
+ " - 本地分支和远程分支分组显示",
27
+ " - 如果分支很多,可以只显示最近的活跃分支(用 `--sort=-committerdate` 排序并限制数量)",
28
+ "4. 向用户展示格式化后的分支列表,清楚标明当前所在分支",
29
+ "",
30
+ "可选增强:",
31
+ "- 运行 `git branch -v` 显示每个分支的最新提交摘要",
32
+ "- 用表格形式展示,包含:分支名、最新提交、是否已合并到主分支"
33
+ ].join("\n");
34
+ }
35
+ /** 构建创建分支的指令。 */
36
+ function buildCreateInstruction(name, base) {
37
+ if (!name || name.trim().length === 0) return "错误:创建分支需要指定分支名称。用法:/branch create <name> [base]";
38
+ const baseRef = base && base.trim().length > 0 ? base.trim() : "";
39
+ const baseDesc = baseRef ? `(基于 ${baseRef})` : "(基于当前分支)";
40
+ return [
41
+ `# 创建 Git 分支:${name.trim()} ${baseDesc}`,
42
+ "",
43
+ "请按以下步骤创建新分支:",
44
+ "",
45
+ baseRef ? `1. 运行 \`git checkout -b ${name.trim()} ${baseRef}\` 创建并切换到新分支` : `1. 运行 \`git checkout -b ${name.trim()}\` 创建并切换到新分支`,
46
+ "2. 确认当前已切换到新分支:运行 `git branch --show-current`",
47
+ "3. 向用户报告:已创建并切换到分支 `${name.trim()}`"
48
+ ].join("\n");
49
+ }
50
+ /** 构建切换分支的指令。 */
51
+ function buildSwitchInstruction(name) {
52
+ if (!name || name.trim().length === 0) return "错误:切换分支需要指定分支名称。用法:/branch switch <name>";
53
+ return [
54
+ `# 切换到分支:${name.trim()}`,
55
+ "",
56
+ "请按以下步骤切换分支:",
57
+ "",
58
+ "1. 先运行 `git status` 检查当前工作区是否有未提交的变更",
59
+ "2. 如果有未提交变更:",
60
+ " - 运行 `git stash` 暂存当前变更,或",
61
+ " - 提示用户是否需要先提交或放弃变更",
62
+ `3. 运行 \`git checkout ${name.trim()}\`(或 \`git switch ${name.trim()}\`)切换到目标分支`,
63
+ "4. 运行 `git branch --show-current` 确认切换成功",
64
+ "5. 如果之前有 stash,提示用户可用 `git stash pop` 恢复",
65
+ "",
66
+ "注意事项:",
67
+ "- 如果分支不存在,检查远程分支:`git branch -r | grep ${name.trim()}`",
68
+ "- 远程分支存在时,用 `git checkout -b ${name.trim()} origin/${name.trim()}` 创建本地跟踪分支"
69
+ ].join("\n");
70
+ }
71
+ /** 构建删除分支的指令。 */
72
+ function buildDeleteInstruction(name) {
73
+ if (!name || name.trim().length === 0) return "错误:删除分支需要指定分支名称。用法:/branch delete <name>";
74
+ return [
75
+ `# 删除 Git 分支:${name.trim()}`,
76
+ "",
77
+ "请按以下步骤删除分支:",
78
+ "",
79
+ "1. 先确认当前不在目标分支上(运行 `git branch --show-current`)",
80
+ " - 如果当前在目标分支上,先切换到其他分支(如 master)",
81
+ `2. 运行 \`git branch -d ${name.trim()}\` 删除本地分支`,
82
+ "3. 如果提示「未合并」:",
83
+ " - 警告用户该分支还有未合并的提交,列出未合并的提交",
84
+ " - 询问用户确认后才使用 `git branch -D ${name.trim()}` 强制删除",
85
+ "4. 删除成功后向用户报告",
86
+ "",
87
+ "注意事项:",
88
+ "- 安全删除(-d)优先,只有用户明确确认后才用强制删除(-D)",
89
+ "- 删除前展示该分支的最后 3 条提交记录,帮助用户确认",
90
+ "- 如果有对应的远程分支,询问用户是否也需要删除:`git push origin --delete ${name.trim()}`"
91
+ ].join("\n");
92
+ }
93
+ //#endregion
94
+ export { handleBranchCommand };
95
+
96
+ //# sourceMappingURL=git-branch-Dn1CP6An.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-branch-Dn1CP6An.mjs","names":[],"sources":["../src/commands/git-branch.ts"],"sourcesContent":["/**\n * /branch 命令 — Git 分支管理。\n *\n * 生成中文指令,引导模型执行分支操作:\n * - list:列出所有分支并高亮当前分支\n * - create:从指定基准创建新分支\n * - switch:切换到指定分支\n * - delete:删除分支(未合并时警告)\n */\n\n// ── Types ────────────────────────────────────────────\n\nexport interface BranchCommandArgs {\n /** 操作类型:列出 / 创建 / 切换 / 删除。默认为 list。 */\n action?: \"list\" | \"create\" | \"switch\" | \"delete\";\n /** 分支名称(create / switch / delete 时必填)。 */\n name?: string;\n /** 创建分支时的基准分支(仅 create 时有效)。 */\n base?: string;\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * 处理 /branch 命令,返回一段中文指令引导模型完成分支操作。\n */\nexport function handleBranchCommand(args: BranchCommandArgs): { instruction: string } {\n const { action = \"list\", name, base } = args;\n\n switch (action) {\n case \"list\":\n return { instruction: buildListInstruction() };\n case \"create\":\n return { instruction: buildCreateInstruction(name, base) };\n case \"switch\":\n return { instruction: buildSwitchInstruction(name) };\n case \"delete\":\n return { instruction: buildDeleteInstruction(name) };\n default:\n return {\n instruction: `未知操作:${action}。可选操作:list、create、switch、delete`,\n };\n }\n}\n\n// ── Instruction Builders ─────────────────────────────\n\n/** 构建列出分支的指令。 */\nfunction buildListInstruction(): string {\n const steps: string[] = [\n \"# Git 分支列表\",\n \"\",\n \"请执行以下步骤列出所有分支:\",\n \"\",\n \"1. 运行 `git branch -a` 列出所有本地和远程分支\",\n \"2. 运行 `git branch --show-current` 获取当前分支名\",\n \"3. 格式化输出:\",\n \" - 当前分支用 `*` 或特殊标记高亮显示\",\n \" - 本地分支和远程分支分组显示\",\n \" - 如果分支很多,可以只显示最近的活跃分支(用 `--sort=-committerdate` 排序并限制数量)\",\n \"4. 向用户展示格式化后的分支列表,清楚标明当前所在分支\",\n \"\",\n \"可选增强:\",\n \"- 运行 `git branch -v` 显示每个分支的最新提交摘要\",\n \"- 用表格形式展示,包含:分支名、最新提交、是否已合并到主分支\",\n ];\n\n return steps.join(\"\\n\");\n}\n\n/** 构建创建分支的指令。 */\nfunction buildCreateInstruction(name?: string, base?: string): string {\n if (!name || name.trim().length === 0) {\n return \"错误:创建分支需要指定分支名称。用法:/branch create <name> [base]\";\n }\n\n const baseRef = base && base.trim().length > 0 ? base.trim() : \"\";\n const baseDesc = baseRef ? `(基于 ${baseRef})` : \"(基于当前分支)\";\n\n const steps: string[] = [\n `# 创建 Git 分支:${name.trim()} ${baseDesc}`,\n \"\",\n \"请按以下步骤创建新分支:\",\n \"\",\n baseRef\n ? `1. 运行 \\`git checkout -b ${name.trim()} ${baseRef}\\` 创建并切换到新分支`\n : `1. 运行 \\`git checkout -b ${name.trim()}\\` 创建并切换到新分支`,\n \"2. 确认当前已切换到新分支:运行 `git branch --show-current`\",\n \"3. 向用户报告:已创建并切换到分支 `${name.trim()}`\",\n ];\n\n return steps.join(\"\\n\");\n}\n\n/** 构建切换分支的指令。 */\nfunction buildSwitchInstruction(name?: string): string {\n if (!name || name.trim().length === 0) {\n return \"错误:切换分支需要指定分支名称。用法:/branch switch <name>\";\n }\n\n const steps: string[] = [\n `# 切换到分支:${name.trim()}`,\n \"\",\n \"请按以下步骤切换分支:\",\n \"\",\n \"1. 先运行 `git status` 检查当前工作区是否有未提交的变更\",\n \"2. 如果有未提交变更:\",\n \" - 运行 `git stash` 暂存当前变更,或\",\n \" - 提示用户是否需要先提交或放弃变更\",\n `3. 运行 \\`git checkout ${name.trim()}\\`(或 \\`git switch ${name.trim()}\\`)切换到目标分支`,\n \"4. 运行 `git branch --show-current` 确认切换成功\",\n \"5. 如果之前有 stash,提示用户可用 `git stash pop` 恢复\",\n \"\",\n \"注意事项:\",\n \"- 如果分支不存在,检查远程分支:`git branch -r | grep ${name.trim()}`\",\n \"- 远程分支存在时,用 `git checkout -b ${name.trim()} origin/${name.trim()}` 创建本地跟踪分支\",\n ];\n\n return steps.join(\"\\n\");\n}\n\n/** 构建删除分支的指令。 */\nfunction buildDeleteInstruction(name?: string): string {\n if (!name || name.trim().length === 0) {\n return \"错误:删除分支需要指定分支名称。用法:/branch delete <name>\";\n }\n\n const steps: string[] = [\n `# 删除 Git 分支:${name.trim()}`,\n \"\",\n \"请按以下步骤删除分支:\",\n \"\",\n \"1. 先确认当前不在目标分支上(运行 `git branch --show-current`)\",\n \" - 如果当前在目标分支上,先切换到其他分支(如 master)\",\n `2. 运行 \\`git branch -d ${name.trim()}\\` 删除本地分支`,\n \"3. 如果提示「未合并」:\",\n \" - 警告用户该分支还有未合并的提交,列出未合并的提交\",\n \" - 询问用户确认后才使用 `git branch -D ${name.trim()}` 强制删除\",\n \"4. 删除成功后向用户报告\",\n \"\",\n \"注意事项:\",\n \"- 安全删除(-d)优先,只有用户明确确认后才用强制删除(-D)\",\n \"- 删除前展示该分支的最后 3 条提交记录,帮助用户确认\",\n \"- 如果有对应的远程分支,询问用户是否也需要删除:`git push origin --delete ${name.trim()}`\",\n ];\n\n return steps.join(\"\\n\");\n}\n"],"mappings":";;;;AA0BA,SAAgB,oBAAoB,MAAkD;CACpF,MAAM,EAAE,SAAS,QAAQ,MAAM,SAAS;CAExC,QAAQ,QAAR;EACE,KAAK,QACH,OAAO,EAAE,aAAa,qBAAqB,EAAE;EAC/C,KAAK,UACH,OAAO,EAAE,aAAa,uBAAuB,MAAM,IAAI,EAAE;EAC3D,KAAK,UACH,OAAO,EAAE,aAAa,uBAAuB,IAAI,EAAE;EACrD,KAAK,UACH,OAAO,EAAE,aAAa,uBAAuB,IAAI,EAAE;EACrD,SACE,OAAO,EACL,aAAa,QAAQ,OAAO,iCAC9B;CACJ;AACF;;AAKA,SAAS,uBAA+B;CAmBtC,OAAO;EAjBL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAGS,CAAC,CAAC,KAAK,IAAI;AACxB;;AAGA,SAAS,uBAAuB,MAAe,MAAuB;CACpE,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,WAAW,GAClC,OAAO;CAGT,MAAM,UAAU,QAAQ,KAAK,KAAK,CAAC,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI;CAC/D,MAAM,WAAW,UAAU,OAAO,QAAQ,KAAK;CAc/C,OAAO;EAXL,eAAe,KAAK,KAAK,EAAE,GAAG;EAC9B;EACA;EACA;EACA,UACI,2BAA2B,KAAK,KAAK,EAAE,GAAG,QAAQ,gBAClD,2BAA2B,KAAK,KAAK,EAAE;EAC3C;EACA;CAGS,CAAC,CAAC,KAAK,IAAI;AACxB;;AAGA,SAAS,uBAAuB,MAAuB;CACrD,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,WAAW,GAClC,OAAO;CAqBT,OAAO;EAjBL,WAAW,KAAK,KAAK;EACrB;EACA;EACA;EACA;EACA;EACA;EACA;EACA,wBAAwB,KAAK,KAAK,EAAE,oBAAoB,KAAK,KAAK,EAAE;EACpE;EACA;EACA;EACA;EACA;EACA;CAGS,CAAC,CAAC,KAAK,IAAI;AACxB;;AAGA,SAAS,uBAAuB,MAAuB;CACrD,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,WAAW,GAClC,OAAO;CAsBT,OAAO;EAlBL,eAAe,KAAK,KAAK;EACzB;EACA;EACA;EACA;EACA;EACA,yBAAyB,KAAK,KAAK,EAAE;EACrC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAGS,CAAC,CAAC,KAAK,IAAI;AACxB"}
@@ -0,0 +1,171 @@
1
+ import { d as runPhase2WithContext, f as bootstrap, t as installProcessLifecycle } from "./process-lifecycle-Dg6n2QS-.mjs";
2
+ import { i as resolveProvider, n as loadConfig, r as resolveConfigEnv } from "./config-Des0z-k9.mjs";
3
+ import { asMessageId, resolvePaths } from "@lynx/core";
4
+ import { dirname, join } from "node:path";
5
+ import "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ //#region src/headless-launcher.ts
8
+ /**
9
+ * headless-launcher — non-interactive mode for Lynx.
10
+ *
11
+ * Used when `lynx start --headless` is invoked. Starts the agent engine
12
+ * and optionally a messaging channel (飞书 etc.) for remote interaction.
13
+ * No TUI is rendered — all I/O goes through the channel adapter.
14
+ *
15
+ * Responsibility:
16
+ * 1. Bootstrap the application (same as tui-launcher)
17
+ * 2. Register channel adapters (飞书 etc.)
18
+ * 3. Process incoming messages through the engine
19
+ * 4. Stream responses back through the channel
20
+ */
21
+ /**
22
+ * Start Lynx in headless (non-interactive) mode.
23
+ *
24
+ * If a 飞书 configuration is provided, the agent listens for
25
+ * incoming messages from the 飞书 channel and responds via
26
+ * the same channel. Otherwise, runs a simple REPL on stdin/stdout.
27
+ */
28
+ async function startHeadless(config = {}) {
29
+ const paths = resolvePaths();
30
+ const appConfig = loadConfig();
31
+ resolveConfigEnv(appConfig);
32
+ const provider = resolveProvider(appConfig);
33
+ const builtinSkillsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "lynx-agent", "skills");
34
+ const ctx = bootstrap({
35
+ homeDir: paths.home,
36
+ provider,
37
+ model: config.model ?? process.env.LYNX_MODEL ?? appConfig.model,
38
+ skillsDir: builtinSkillsDir,
39
+ workspace: process.cwd(),
40
+ feishu: config.feishu
41
+ });
42
+ const sessions = ctx.sessionMgr.list();
43
+ const activeSession = sessions.length > 0 ? sessions[0] : ctx.sessionMgr.create("default", process.cwd());
44
+ let abortLayer = 0;
45
+ let isStreaming = false;
46
+ const getAbortLayer = () => abortLayer;
47
+ const incrementAbortLayer = () => {
48
+ abortLayer++;
49
+ return abortLayer;
50
+ };
51
+ const resetAbortLayer = () => {
52
+ abortLayer = 0;
53
+ };
54
+ installProcessLifecycle({
55
+ ctx,
56
+ onAbortLl: () => ctx.engine.abort(),
57
+ onAbortTool: () => ctx.engine.abort(),
58
+ getAbortLayer,
59
+ incrementAbortLayer,
60
+ resetAbortLayer,
61
+ isStreaming: () => isStreaming
62
+ });
63
+ runPhase2WithContext(ctx, paths).catch(() => {});
64
+ async function processMessage(msg) {
65
+ isStreaming = true;
66
+ resetAbortLayer();
67
+ const userMsg = {
68
+ id: asMessageId(crypto.randomUUID()),
69
+ role: "user",
70
+ content: [{
71
+ type: "text",
72
+ text: msg.text
73
+ }],
74
+ timestamp: Date.now(),
75
+ turnIndex: activeSession.messages.length
76
+ };
77
+ activeSession.messages.push(userMsg);
78
+ const controller = new AbortController();
79
+ let responseText = "";
80
+ try {
81
+ for await (const event of ctx.engine.submit(activeSession, userMsg, controller.signal)) switch (event.type) {
82
+ case "text_delta":
83
+ responseText += event.text;
84
+ break;
85
+ case "reasoning_delta": break;
86
+ case "tool_use_start":
87
+ if (config.verbose) process.stderr.write(`[headless] Tool call: ${event.name}\n`);
88
+ break;
89
+ case "tool_result":
90
+ if (config.verbose) {
91
+ const truncated = event.content.length > 200 ? event.content.slice(0, 200) + "..." : event.content;
92
+ process.stderr.write(`[headless] Tool result: ${truncated}\n`);
93
+ }
94
+ break;
95
+ case "error":
96
+ responseText += `\nError: ${event.message}`;
97
+ break;
98
+ case "done": break;
99
+ }
100
+ } catch (err) {
101
+ responseText += `\nError: ${err instanceof Error ? err.message : String(err)}`;
102
+ } finally {
103
+ isStreaming = false;
104
+ }
105
+ if (responseText) {
106
+ const reply = {
107
+ id: asMessageId(crypto.randomUUID()),
108
+ role: "assistant",
109
+ content: [{
110
+ type: "text",
111
+ text: responseText
112
+ }],
113
+ timestamp: Date.now(),
114
+ turnIndex: activeSession.messages.length
115
+ };
116
+ activeSession.messages.push(reply);
117
+ }
118
+ return responseText;
119
+ }
120
+ const channelIds = ctx.channelRegistry.list();
121
+ if (channelIds.length > 0) {
122
+ process.stderr.write(`[headless] Listening on channels: ${channelIds.join(", ")}\n`);
123
+ const primaryChannelId = channelIds[0];
124
+ const adapter = ctx.channelRegistry.get(primaryChannelId);
125
+ if (!adapter) {
126
+ process.stderr.write(`[headless] Channel "${primaryChannelId}" not found\n`);
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ adapter.onMessage((msg) => {
131
+ const textContent = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
132
+ if (!textContent) return;
133
+ processMessage({ text: textContent }).then(async (replyText) => {
134
+ if (replyText) {
135
+ const replyMsg = {
136
+ id: asMessageId(crypto.randomUUID()),
137
+ role: "assistant",
138
+ content: [{
139
+ type: "text",
140
+ text: replyText
141
+ }],
142
+ timestamp: Date.now(),
143
+ turnIndex: activeSession.messages.length
144
+ };
145
+ await adapter.sendMessage(replyMsg);
146
+ }
147
+ }).catch((err) => {
148
+ process.stderr.write(`[headless] Channel message processing failed: ${err instanceof Error ? err.message : String(err)}\n`);
149
+ });
150
+ });
151
+ await new Promise(() => {});
152
+ } else {
153
+ process.stderr.write("[headless] No channels configured — stdin mode\n");
154
+ process.stderr.write("[headless] Enter your message (Ctrl+D to end):\n");
155
+ const chunks = [];
156
+ process.stdin.setEncoding("utf-8");
157
+ for await (const chunk of process.stdin) chunks.push(chunk);
158
+ const text = chunks.join("").trim();
159
+ if (!text) {
160
+ process.stderr.write("[headless] Empty input — exiting\n");
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ const replyText = await processMessage({ text });
165
+ process.stdout.write(replyText + "\n");
166
+ }
167
+ }
168
+ //#endregion
169
+ export { startHeadless };
170
+
171
+ //# sourceMappingURL=headless-launcher-I8NWyD6k.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-launcher-I8NWyD6k.mjs","names":[],"sources":["../src/headless-launcher.ts"],"sourcesContent":["/**\n * headless-launcher — non-interactive mode for Lynx.\n *\n * Used when `lynx start --headless` is invoked. Starts the agent engine\n * and optionally a messaging channel (飞书 etc.) for remote interaction.\n * No TUI is rendered — all I/O goes through the channel adapter.\n *\n * Responsibility:\n * 1. Bootstrap the application (same as tui-launcher)\n * 2. Register channel adapters (飞书 etc.)\n * 3. Process incoming messages through the engine\n * 4. Stream responses back through the channel\n */\n\nimport { resolvePaths, asMessageId } from \"@lynx/core\";\nimport { loadConfig, resolveConfigEnv, resolveProvider } from \"./commands/config.js\";\nimport { bootstrap } from \"./bootstrap.js\";\nimport { runPhase2WithContext } from \"./startup.js\";\nimport { installProcessLifecycle } from \"./process-lifecycle.js\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { homedir } from \"node:os\";\n\n// ── Types ────────────────────────────────────────────\n\nexport interface HeadlessConfig {\n /** Model to use (overrides LYNX_MODEL env var). */\n model?: string;\n /** Enable verbose logging. */\n verbose?: boolean;\n /** 飞书 channel configuration. */\n feishu?: { appId: string; appSecret: string };\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Start Lynx in headless (non-interactive) mode.\n *\n * If a 飞书 configuration is provided, the agent listens for\n * incoming messages from the 飞书 channel and responds via\n * the same channel. Otherwise, runs a simple REPL on stdin/stdout.\n */\nexport async function startHeadless(config: HeadlessConfig = {}): Promise<void> {\n const paths = resolvePaths();\n\n // 加载配置 → 注入 env → 按模型名自动选 Provider\n const appConfig = loadConfig();\n resolveConfigEnv(appConfig);\n const provider = resolveProvider(appConfig);\n\n // Resolve built‑in skills directory relative to the lynx-agent package\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n const builtinSkillsDir = join(__dirname, \"..\", \"..\", \"lynx-agent\", \"skills\");\n\n const ctx = bootstrap({\n homeDir: paths.home,\n provider,\n model: config.model ?? process.env.LYNX_MODEL ?? appConfig.model,\n skillsDir: builtinSkillsDir,\n workspace: process.cwd(),\n feishu: config.feishu,\n });\n\n const sessions = ctx.sessionMgr.list();\n const activeSession =\n sessions.length > 0 ? sessions[0]! : ctx.sessionMgr.create(\"default\", process.cwd());\n\n // ── Abort layer state ────────────────────────────\n let abortLayer = 0;\n let isStreaming = false;\n\n const getAbortLayer = () => abortLayer;\n const incrementAbortLayer = () => {\n abortLayer++;\n return abortLayer;\n };\n const resetAbortLayer = () => {\n abortLayer = 0;\n };\n\n // ── Process lifecycle ───────────────────────────\n installProcessLifecycle({\n ctx,\n onAbortLl: () => ctx.engine.abort(),\n onAbortTool: () => ctx.engine.abort(),\n getAbortLayer,\n incrementAbortLayer,\n resetAbortLayer,\n isStreaming: () => isStreaming,\n });\n\n // ── Phase 2: background tasks ───────────────────\n runPhase2WithContext(ctx, paths).catch(() => {\n // Errors are already logged by the task runner\n });\n\n // ── Message handler (shared across all channels) ─\n async function processMessage(msg: { text: string }): Promise<string> {\n isStreaming = true;\n resetAbortLayer();\n\n const userMsg = {\n id: asMessageId(crypto.randomUUID()),\n role: \"user\" as const,\n content: [{ type: \"text\" as const, text: msg.text }],\n timestamp: Date.now(),\n turnIndex: activeSession.messages.length,\n };\n\n // Add user message to the session\n activeSession.messages.push(userMsg);\n\n const controller = new AbortController();\n let responseText = \"\";\n\n try {\n for await (const event of ctx.engine.submit(activeSession, userMsg, controller.signal)) {\n switch (event.type) {\n case \"text_delta\":\n responseText += event.text;\n break;\n case \"reasoning_delta\":\n // Reasoning tokens — skip in headless mode (not sent to channel)\n break;\n case \"tool_use_start\":\n if (config.verbose) {\n process.stderr.write(`[headless] Tool call: ${event.name}\\n`);\n }\n break;\n case \"tool_result\":\n if (config.verbose) {\n const truncated =\n event.content.length > 200 ? event.content.slice(0, 200) + \"...\" : event.content;\n process.stderr.write(`[headless] Tool result: ${truncated}\\n`);\n }\n break;\n case \"error\":\n responseText += `\\nError: ${event.message}`;\n break;\n case \"done\":\n break;\n }\n }\n } catch (err) {\n responseText += `\\nError: ${err instanceof Error ? err.message : String(err)}`;\n } finally {\n isStreaming = false;\n }\n\n // Add assistant response to the session\n if (responseText) {\n const reply = {\n id: asMessageId(crypto.randomUUID()),\n role: \"assistant\" as const,\n content: [{ type: \"text\" as const, text: responseText }],\n timestamp: Date.now(),\n turnIndex: activeSession.messages.length,\n };\n activeSession.messages.push(reply);\n }\n\n return responseText;\n }\n\n // ── Channel or stdin mode ───────────────────────\n const channelIds = ctx.channelRegistry.list();\n\n if (channelIds.length > 0) {\n // Channel mode — use the first registered channel for I/O.\n // The registry wires each adapter's onMessage() to the centralized handler.\n // Since Message doesn't carry a channelId, we get the adapter directly\n // and bypass the centralized handler for request/response pairing.\n process.stderr.write(`[headless] Listening on channels: ${channelIds.join(\", \")}\\n`);\n\n const primaryChannelId = channelIds[0]!;\n const adapter = ctx.channelRegistry.get(primaryChannelId);\n\n if (!adapter) {\n process.stderr.write(`[headless] Channel \"${primaryChannelId}\" not found\\n`);\n process.exitCode = 1;\n return;\n }\n\n // Listen for incoming messages on the primary channel\n adapter.onMessage((msg) => {\n const textContent = msg.content\n .filter((c) => c.type === \"text\")\n .map((c) => (c as { text: string }).text)\n .join(\"\\n\");\n\n if (!textContent) return;\n\n processMessage({ text: textContent })\n .then(async (replyText) => {\n if (replyText) {\n const replyMsg = {\n id: asMessageId(crypto.randomUUID()),\n role: \"assistant\" as const,\n content: [{ type: \"text\" as const, text: replyText }],\n timestamp: Date.now(),\n turnIndex: activeSession.messages.length,\n };\n await adapter.sendMessage(replyMsg);\n }\n })\n .catch((err) => {\n process.stderr.write(\n `[headless] Channel message processing failed: ` +\n `${err instanceof Error ? err.message : String(err)}\\n`,\n );\n });\n });\n\n // Keep process alive — channels run indefinitely\n await new Promise<void>(() => {\n // Never resolves — process runs until SIGTERM/SIGINT\n });\n } else {\n // Stdin mode — read one message and respond\n process.stderr.write(\"[headless] No channels configured — stdin mode\\n\");\n process.stderr.write(\"[headless] Enter your message (Ctrl+D to end):\\n\");\n\n const chunks: string[] = [];\n process.stdin.setEncoding(\"utf-8\");\n\n for await (const chunk of process.stdin) {\n chunks.push(chunk as string);\n }\n\n const text = chunks.join(\"\").trim();\n if (!text) {\n process.stderr.write(\"[headless] Empty input — exiting\\n\");\n process.exitCode = 1;\n return;\n }\n\n const replyText = await processMessage({ text });\n process.stdout.write(replyText + \"\\n\");\n }\n}\n"],"mappings":"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"}