@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.2

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 (67) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +42 -12
  3. package/README.zh.md +42 -12
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +17 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +92 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +643 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +7 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +172 -23
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/index.js +134 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/proxy/ProxySessionController.js +436 -12
  39. package/dist/proxy/ProxySessionController.js.map +1 -1
  40. package/dist/state/StateStore.js +18 -0
  41. package/dist/state/StateStore.js.map +1 -1
  42. package/docs/technical-reference.md +17 -7
  43. package/docs/technical-reference.zh.md +17 -7
  44. package/npm-shrinkwrap.json +2 -2
  45. package/package.json +2 -2
  46. package/skills/agent-bot/SKILL.md +107 -188
  47. package/src/cli/AutostartManager.ts +579 -0
  48. package/src/cli/FeishuAppConfiguration.ts +22 -7
  49. package/src/cli/Initializer.ts +57 -0
  50. package/src/cli/SelfUpdateRunner.ts +817 -0
  51. package/src/cli/SelfUpdater.ts +688 -0
  52. package/src/cli/autostartOutput.ts +75 -0
  53. package/src/cli/controlProtocol.ts +111 -1
  54. package/src/cli/help.ts +92 -18
  55. package/src/cli/taskGroupOptions.ts +27 -17
  56. package/src/cli/taskListOutput.ts +10 -0
  57. package/src/cli/taskTarget.ts +168 -0
  58. package/src/cli.ts +781 -65
  59. package/src/codex/CodexRuntime.ts +8 -2
  60. package/src/commands/CommandRouter.ts +6 -1
  61. package/src/commands/commandTypes.ts +1 -0
  62. package/src/feishu/CardRenderer.ts +199 -23
  63. package/src/feishu/FeishuMarkdown.ts +27 -1
  64. package/src/index.ts +172 -2
  65. package/src/proxy/ProxySessionController.ts +532 -15
  66. package/src/runtime/types.ts +1 -0
  67. package/src/state/StateStore.ts +19 -0
@@ -355,12 +355,18 @@ export class CodexRuntime implements AgentRuntime {
355
355
 
356
356
  async closeSession(sessionId: string): Promise<void> {
357
357
  const session = this.requireSession(sessionId);
358
+ await this.archiveRemoteSession(session.remoteSessionId);
359
+ }
360
+
361
+ async archiveRemoteSession(remoteSessionId: string): Promise<void> {
358
362
  await (await this.client()).request(
359
363
  "thread/archive",
360
- { threadId: session.remoteSessionId },
364
+ { threadId: remoteSessionId },
361
365
  CONTROL_REQUEST_TIMEOUT_MS,
362
366
  );
363
- this.sessions.delete(sessionId);
367
+ for (const [localSessionId, session] of this.sessions) {
368
+ if (session.remoteSessionId === remoteSessionId) this.sessions.delete(localSessionId);
369
+ }
364
370
  }
365
371
 
366
372
  async setTitle(sessionId: string, title: string): Promise<void> {
@@ -1,7 +1,8 @@
1
1
  import type { Command } from "./commandTypes.js";
2
2
 
3
- const COMMAND_NAMES = [
3
+ export const COMMAND_NAMES = [
4
4
  "agent",
5
+ "archive",
5
6
  "dir",
6
7
  "fork",
7
8
  "forkgroup",
@@ -76,6 +77,10 @@ export class CommandRouter {
76
77
  return args[0] ? { type: "switch", sessionId: args[0] } : { type: "switch" };
77
78
  case "agent":
78
79
  return args[0] ? { type: "agent", agent: args[0] } : { type: "agent" };
80
+ case "archive":
81
+ rejectOptions("/archive", args);
82
+ if (args.length > 1) throw new Error("/archive 只接受一个可选的任务序号或任务 ID。");
83
+ return args[0] ? { type: "archive", sessionId: args[0] } : { type: "archive" };
79
84
  case "stop":
80
85
  return { type: "stop" };
81
86
  case "status":
@@ -10,6 +10,7 @@ export type Command =
10
10
  | { type: "sessions"; searchTerm?: string }
11
11
  | { type: "switch"; sessionId?: string }
12
12
  | { type: "agent"; agent?: string }
13
+ | { type: "archive"; sessionId?: string }
13
14
  | { type: "stop" }
14
15
  | { type: "status"; sessionId?: string }
15
16
  | { type: "goal"; action: "show" }
@@ -11,7 +11,7 @@ import type {
11
11
  ToolState,
12
12
  } from "../runtime/types.js";
13
13
  import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
14
- import { truncateMiddle, truncateText } from "../utils/markdown.js";
14
+ import { truncateText } from "../utils/markdown.js";
15
15
  import { localCardImage } from "./LocalCardImage.js";
16
16
 
17
17
  export interface StartupStatusView {
@@ -889,15 +889,26 @@ export class CardRenderer {
889
889
 
890
890
  renderActivityHistory(state: TurnViewState, requestedPage: number): Record<string, unknown> {
891
891
  const grouped = this.thinkingCardLayout === "grouped";
892
+ const activities = turnActivities(state);
893
+ const allGroups = grouped ? groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity) : [];
894
+ const historyGroups = grouped
895
+ ? groupedLiveActivityPage(groupedActivityPagesFromGroups(allGroups, state.projectCwd), state.projectCwd).historyGroups
896
+ : [];
897
+ const usesHistoricalPrefixPages = historyGroups.length > 0;
892
898
  const pages = grouped
893
- ? groupedActivityPages(turnActivities(state), state.projectCwd, {
899
+ ? groupedActivityPagesFromGroups(
900
+ usesHistoricalPrefixPages ? historyGroups : allGroups,
901
+ state.projectCwd,
902
+ {
894
903
  fullActivityText: true,
895
904
  includeReasoningHistory: true,
896
- })
897
- : activityPages(turnActivities(state));
905
+ },
906
+ )
907
+ : activityPages(activities);
898
908
  if (pages.length === 0) {
899
909
  return this.renderSectionsCard("思考活动历史", [{ lines: ["没有保存到活动记录。"] }]);
900
910
  }
911
+ const totalPages = usesHistoricalPrefixPages ? pages.length + 1 : pages.length;
901
912
  const page = Math.max(0, Math.min(Math.trunc(requestedPage), pages.length - 1));
902
913
  const actions: TaskListCardAction[] = [
903
914
  ...(pages.length > 1 ? [{
@@ -908,7 +919,7 @@ export class CardRenderer {
908
919
  text: "上一页",
909
920
  value: { action: "activity_history", turnId: state.turnId, page: String(page - 1) },
910
921
  }] : []),
911
- ...(page < pages.length - 2 ? [{
922
+ ...(page < pages.length - (usesHistoricalPrefixPages ? 1 : 2) ? [{
912
923
  text: "下一页",
913
924
  value: { action: "activity_history", turnId: state.turnId, page: String(page + 1) },
914
925
  }] : []),
@@ -921,7 +932,7 @@ export class CardRenderer {
921
932
  includeReasoningHistory: true,
922
933
  })
923
934
  : renderActivities(pages[page] as TurnActivity[] | undefined ?? [], state.projectCwd, true)));
924
- return sectionCard(`思考活动历史 · ${page + 1}/${pages.length}`, elements.length > 0 ? elements : [markdown("无")]);
935
+ return sectionCard(`思考活动历史 · ${page + 1}/${totalPages}`, elements.length > 0 ? elements : [markdown("无")]);
925
936
  }
926
937
 
927
938
  renderSessionStarted(session: RuntimeSession): Record<string, unknown> {
@@ -1249,24 +1260,24 @@ function renderGroupedTurnElements(
1249
1260
  const elements: Record<string, unknown>[] = [];
1250
1261
  const allActivities = turnActivities(state);
1251
1262
  const livePages = groupedActivityPages(allActivities, state.projectCwd);
1252
- const historyPages = groupedActivityPages(allActivities, state.projectCwd, {
1263
+ const visible = groupedLiveActivityPage(livePages, state.projectCwd);
1264
+ const historyPages = groupedActivityPagesFromGroups(visible.historyGroups, state.projectCwd, {
1253
1265
  fullActivityText: true,
1254
1266
  includeReasoningHistory: true,
1255
1267
  });
1256
- const visibleGroups = livePages.at(-1) ?? [];
1257
1268
  if (state.plan.length > 0) elements.push(planPanel(state.plan));
1258
- if (historyPages.length > 1) {
1269
+ if (historyPages.length > 0) {
1259
1270
  elements.push(taskActionRow([{
1260
- text: `查看历史思考(共 ${historyPages.length} 页)`,
1271
+ text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
1261
1272
  value: {
1262
1273
  action: "activity_history",
1263
1274
  turnId: state.turnId,
1264
- page: String(historyPages.length - 2),
1275
+ page: String(historyPages.length - 1),
1265
1276
  },
1266
1277
  }]));
1267
1278
  }
1268
- if (state.activitiesTruncated || livePages.length > 1 || historyPages.length > 1) elements.push(markdown("…"));
1269
- elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
1279
+ if (state.activitiesTruncated && visible.groups[0]?.kind !== "gap") elements.push(markdown("…"));
1280
+ elements.push(...renderGroupedActivityGroups(visible.groups, state.projectCwd));
1270
1281
  if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
1271
1282
 
1272
1283
  if (state.approval) {
@@ -1407,6 +1418,7 @@ function renderActivities(
1407
1418
 
1408
1419
  type GroupedTurnActivity =
1409
1420
  | { kind: "activity"; activity: TurnActivity }
1421
+ | { kind: "gap"; id: string }
1410
1422
  | {
1411
1423
  kind: "execution";
1412
1424
  id: string;
@@ -1424,6 +1436,7 @@ function renderGroupedActivityGroups(
1424
1436
  } = {},
1425
1437
  ): Record<string, unknown>[] {
1426
1438
  return groups.flatMap((group) => {
1439
+ if (group.kind === "gap") return [markdown("…")];
1427
1440
  if (group.kind === "activity") {
1428
1441
  return renderActivity(group.activity, projectCwd, options.fullActivityText === true);
1429
1442
  }
@@ -1500,7 +1513,7 @@ function executionActivityPanel(
1500
1513
  {
1501
1514
  elementId: executionActivityPanelElementId(group.id),
1502
1515
  expanded: false,
1503
- borderColor: status === "failed" ? "red" : status === "running" ? "blue" : "grey",
1516
+ borderColor: status === "running" ? "blue" : "grey",
1504
1517
  compact: true,
1505
1518
  },
1506
1519
  );
@@ -1508,7 +1521,6 @@ function executionActivityPanel(
1508
1521
 
1509
1522
  function executionActivityStatus(tools: ToolState[]): ToolState["status"] {
1510
1523
  if (tools.some((tool) => tool.status === "running")) return "running";
1511
- if (tools.some((tool) => tool.status === "failed")) return "failed";
1512
1524
  return "completed";
1513
1525
  }
1514
1526
 
@@ -1516,7 +1528,7 @@ function executionActivityTitle(
1516
1528
  group: Extract<GroupedTurnActivity, { kind: "execution" }>,
1517
1529
  status: ToolState["status"],
1518
1530
  ): string {
1519
- const icon = status === "running" ? "⏳" : status === "failed" ? "❌" : "💭";
1531
+ const icon = status === "running" ? "⏳" : "💭";
1520
1532
  const latestReasoning = group.latestReasoning?.text
1521
1533
  ? removeMarkdownBold(group.latestReasoning.text).replace(/\s+/g, " ").trim()
1522
1534
  : "";
@@ -1524,9 +1536,7 @@ function executionActivityTitle(
1524
1536
  ? truncateText(latestReasoning, 90)
1525
1537
  : status === "running"
1526
1538
  ? "正在执行工具"
1527
- : status === "failed"
1528
- ? "工具执行失败"
1529
- : "已执行工具";
1539
+ : "已执行工具";
1530
1540
  return `${icon} ${summary} · ${group.tools.length} 个工具`;
1531
1541
  }
1532
1542
 
@@ -1559,6 +1569,7 @@ const ACTIVITIES_PER_PAGE = 40;
1559
1569
  const GROUPED_TOOLS_PER_PANEL = 8;
1560
1570
  const GROUPED_PAGE_ACTIVITY_BYTES = 24 * 1024;
1561
1571
  const GROUPED_PAGE_ACTIVITY_COMPONENTS = 160;
1572
+ const PINNED_LIVE_COMMENTARIES = 3;
1562
1573
  const MAX_LIVE_ASSISTANT_TEXT = 2_000;
1563
1574
  const ACTIVITY_TEXT_CHUNK = 2_500;
1564
1575
 
@@ -1583,6 +1594,17 @@ function groupedActivityPages(
1583
1594
  } = {},
1584
1595
  ): GroupedTurnActivity[][] {
1585
1596
  const groups = groupTurnActivities(activities).flatMap(splitGroupedExecutionActivity);
1597
+ return groupedActivityPagesFromGroups(groups, projectCwd, renderOptions);
1598
+ }
1599
+
1600
+ function groupedActivityPagesFromGroups(
1601
+ groups: GroupedTurnActivity[],
1602
+ projectCwd?: string,
1603
+ renderOptions: {
1604
+ fullActivityText?: boolean;
1605
+ includeReasoningHistory?: boolean;
1606
+ } = {},
1607
+ ): GroupedTurnActivity[][] {
1586
1608
  if (groups.length === 0) return [];
1587
1609
 
1588
1610
  const newestFirst: GroupedTurnActivity[][] = [];
@@ -1613,6 +1635,82 @@ function groupedActivityPageFits(
1613
1635
  && countCardComponents(elements) <= GROUPED_PAGE_ACTIVITY_COMPONENTS;
1614
1636
  }
1615
1637
 
1638
+ interface LiveGroupedActivityPage {
1639
+ groups: GroupedTurnActivity[];
1640
+ historyGroups: GroupedTurnActivity[];
1641
+ }
1642
+
1643
+ function groupedLiveActivityPage(
1644
+ pages: GroupedTurnActivity[][],
1645
+ projectCwd?: string,
1646
+ ): LiveGroupedActivityPage {
1647
+ const latest = pages.at(-1) ?? [];
1648
+ if (pages.length <= 1) return { groups: latest, historyGroups: [] };
1649
+
1650
+ const groups = pages.flat();
1651
+ const latestStart = groups.length - latest.length;
1652
+ const commentaryIndexes = groups
1653
+ .map((group, index) => isCommentaryGroup(group) ? index : -1)
1654
+ .filter((index) => index >= 0);
1655
+ if (commentaryIndexes.length === 0) {
1656
+ return { groups: latest, historyGroups: groups.slice(0, latestStart) };
1657
+ }
1658
+
1659
+ const selected = new Set<number>();
1660
+ const pinnedCommentaries = commentaryIndexes.slice(0, PINNED_LIVE_COMMENTARIES);
1661
+ for (const index of pinnedCommentaries) selected.add(index);
1662
+ const latestCommentary = commentaryIndexes.at(-1)!;
1663
+ for (let index = latestCommentary; index < groups.length; index += 1) selected.add(index);
1664
+
1665
+ const renderSelection = (selection: ReadonlySet<number>): LiveGroupedActivityPage => ({
1666
+ groups: groupsWithGaps(groups, selection),
1667
+ historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
1668
+ });
1669
+ const fits = (selection: ReadonlySet<number>): boolean => {
1670
+ const view = renderSelection(selection);
1671
+ return groupedActivityPageFits(view.groups, projectCwd, {});
1672
+ };
1673
+
1674
+ if (!fits(selected)) {
1675
+ const latestExecutions = [...selected]
1676
+ .filter((index) => index > latestCommentary && groups[index]?.kind === "execution")
1677
+ .sort((left, right) => left - right);
1678
+ const newestExecution = latestExecutions.at(-1);
1679
+ for (const index of latestExecutions) {
1680
+ if (fits(selected) || index === newestExecution) break;
1681
+ selected.delete(index);
1682
+ }
1683
+ }
1684
+
1685
+ return renderSelection(selected);
1686
+ }
1687
+
1688
+ function isCommentaryGroup(group: GroupedTurnActivity): boolean {
1689
+ return group.kind === "activity" && isCommentaryActivity(group.activity);
1690
+ }
1691
+
1692
+ function groupsWithGaps(
1693
+ groups: GroupedTurnActivity[],
1694
+ selected: ReadonlySet<number>,
1695
+ ): GroupedTurnActivity[] {
1696
+ const indexes = [...selected].sort((left, right) => left - right);
1697
+ const visible: GroupedTurnActivity[] = [];
1698
+ const append = (group: GroupedTurnActivity): void => {
1699
+ if (group.kind === "gap" && visible.at(-1)?.kind === "gap") return;
1700
+ visible.push(group);
1701
+ };
1702
+ let previous = -1;
1703
+ for (const index of indexes) {
1704
+ if (previous >= 0 && index > previous + 1) {
1705
+ append({ kind: "gap", id: `gap:${previous}:${index}` });
1706
+ }
1707
+ const group = groups[index];
1708
+ if (group) append(group);
1709
+ previous = index;
1710
+ }
1711
+ return visible;
1712
+ }
1713
+
1616
1714
  function countCardComponents(value: unknown): number {
1617
1715
  if (Array.isArray(value)) {
1618
1716
  return value.reduce((total, entry) => total + countCardComponents(entry), 0);
@@ -1791,13 +1889,22 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
1791
1889
  const result = tool.error ?? tool.output ?? fileSummary;
1792
1890
  const normalizedCommand = stripAnsi(command).trim();
1793
1891
  const displayCommand = tool.kind === "command"
1794
- ? unwrapPowerShellCommand(normalizedCommand) ?? normalizedCommand
1892
+ ? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
1795
1893
  : normalizedCommand;
1796
- const commandText = truncateText(displayCommand, 800);
1797
- const resultText = result ? truncateMiddle(stripAnsi(result).trim(), 1_200) : undefined;
1894
+ const commandText = truncateText(displayCommand, 600);
1895
+ const resultText = result ? truncateToolResult(stripAnsi(result).trim()) : undefined;
1798
1896
  return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
1799
1897
  }
1800
1898
 
1899
+ function truncateToolResult(result: string): string {
1900
+ const maxLength = 900;
1901
+ const marker = "\n...\n";
1902
+ const tailLength = 600;
1903
+ if (result.length <= maxLength) return result;
1904
+ const headLength = maxLength - marker.length - tailLength;
1905
+ return `${result.slice(0, headLength)}${marker}${result.slice(-tailLength)}`;
1906
+ }
1907
+
1801
1908
  function displayFilePath(filePath: string, projectCwd?: string): string {
1802
1909
  if (!projectCwd) return filePath;
1803
1910
  const pathApi = usesWindowsPaths(projectCwd, filePath) ? path.win32 : path;
@@ -1826,7 +1933,7 @@ function toolPanelTitle(tool: ToolState): string {
1826
1933
  const command = stripAnsi(tool.command ?? tool.title).trim();
1827
1934
  const meaningfulCommand = tool.kind === "web_search"
1828
1935
  ? tool.title
1829
- : unwrapPowerShellCommand(command) ?? tool.title;
1936
+ : unwrapShellCommand(command) ?? tool.title;
1830
1937
  const duration = toolDuration(tool);
1831
1938
  const prefix = `${icon} `;
1832
1939
  const suffix = duration ? ` · ${duration}` : "";
@@ -1875,6 +1982,10 @@ function formatTurnDuration(durationMs: number): string {
1875
1982
  return `${seconds}s`;
1876
1983
  }
1877
1984
 
1985
+ function unwrapShellCommand(command: string): string | undefined {
1986
+ return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
1987
+ }
1988
+
1878
1989
  function unwrapPowerShellCommand(command: string): string | undefined {
1879
1990
  const executable = command.match(
1880
1991
  /^(?:"[^"]*(?:pwsh|powershell)\.exe"|(?:\S*[\\/])?(?:pwsh|powershell)(?:\.exe)?)(?=\s|$)/i,
@@ -1885,6 +1996,71 @@ function unwrapPowerShellCommand(command: string): string | undefined {
1885
1996
  if (!commandFlag) return undefined;
1886
1997
  const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
1887
1998
  if (!payload) return undefined;
1999
+ return unwrapQuotedPayload(payload);
2000
+ }
2001
+
2002
+ function unwrapPosixShellCommand(command: string): string | undefined {
2003
+ let executable = readCommandWord(command, 0);
2004
+ if (!executable) return undefined;
2005
+
2006
+ if (posixExecutableName(executable.value) === "env") {
2007
+ let cursor = executable.end;
2008
+ while (true) {
2009
+ const candidate = readCommandWord(command, cursor);
2010
+ if (!candidate) return undefined;
2011
+ const name = posixExecutableName(candidate.value);
2012
+ if (isPosixShell(name)) {
2013
+ executable = candidate;
2014
+ break;
2015
+ }
2016
+ if (!candidate.value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(candidate.value)) {
2017
+ return undefined;
2018
+ }
2019
+ cursor = candidate.end;
2020
+ }
2021
+ } else if (!isPosixShell(posixExecutableName(executable.value))) {
2022
+ return undefined;
2023
+ }
2024
+
2025
+ const args = command.slice(executable.end);
2026
+ const commandFlag = /(?:^|\s)(?:-[A-Za-z]*c[A-Za-z]*|--command)(?:\s+|=)/.exec(args);
2027
+ if (!commandFlag) return undefined;
2028
+ const payload = args.slice(commandFlag.index + commandFlag[0].length).trim();
2029
+ if (!payload) return undefined;
2030
+ return unwrapQuotedPayload(payload);
2031
+ }
2032
+
2033
+ function readCommandWord(command: string, start: number): { value: string; end: number } | undefined {
2034
+ let cursor = start;
2035
+ while (cursor < command.length && /\s/.test(command[cursor] ?? "")) cursor += 1;
2036
+ if (cursor >= command.length) return undefined;
2037
+
2038
+ const quote = command[cursor];
2039
+ if (quote === "\"" || quote === "'") {
2040
+ const endQuote = command.indexOf(quote, cursor + 1);
2041
+ if (endQuote < 0) return undefined;
2042
+ return { value: command.slice(cursor + 1, endQuote), end: endQuote + 1 };
2043
+ }
2044
+
2045
+ const word = /^\S+/.exec(command.slice(cursor));
2046
+ if (!word) return undefined;
2047
+ return { value: word[0], end: cursor + word[0].length };
2048
+ }
2049
+
2050
+ function posixExecutableName(executable: string): string {
2051
+ return executable.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase() ?? "";
2052
+ }
2053
+
2054
+ function isPosixShell(executable: string): boolean {
2055
+ return executable === "sh"
2056
+ || executable === "bash"
2057
+ || executable === "zsh"
2058
+ || executable === "dash"
2059
+ || executable === "ksh"
2060
+ || executable === "fish";
2061
+ }
2062
+
2063
+ function unwrapQuotedPayload(payload: string): string {
1888
2064
  const quote = payload[0];
1889
2065
  if ((quote === "\"" || quote === "'") && payload.at(-1) === quote) return payload.slice(1, -1).trim();
1890
2066
  return payload;
@@ -6,6 +6,8 @@ interface FenceState {
6
6
  }
7
7
 
8
8
  const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
9
+ const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<?[^)\r\n]+>?)\)/g;
10
+ const LOCAL_FILE_LINE_REFERENCE = /(:\d+(?::\d+)?)$/;
9
11
 
10
12
  export function normalizeFeishuMarkdown(markdown: string): string {
11
13
  const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
@@ -17,7 +19,7 @@ export function normalizeFeishuMarkdown(markdown: string): string {
17
19
 
18
20
  if (!fence) {
19
21
  const opener = FENCE_OPENER.exec(body);
20
- if (!opener?.[2]) return line;
22
+ if (!opener?.[2]) return `${includeLocalFileLineReferences(body)}${ending}`;
21
23
 
22
24
  const indent = opener[1] ?? "";
23
25
  const delimiter = opener[2];
@@ -38,6 +40,30 @@ export function normalizeFeishuMarkdown(markdown: string): string {
38
40
  }).join("");
39
41
  }
40
42
 
43
+ function includeLocalFileLineReferences(markdown: string): string {
44
+ return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
45
+ if (imageMarker) return link;
46
+
47
+ const target = unwrapMarkdownTarget(rawTarget.trim());
48
+ if (!isLocalFileTarget(target)) return link;
49
+
50
+ const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
51
+ if (!reference || label.endsWith(reference)) return link;
52
+ return `[${label}${reference}](${rawTarget})`;
53
+ });
54
+ }
55
+
56
+ function unwrapMarkdownTarget(target: string): string {
57
+ return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
58
+ }
59
+
60
+ function isLocalFileTarget(target: string): boolean {
61
+ return /^file:\/\//i.test(target)
62
+ || /^\/?[a-z]:[\\/]/i.test(target)
63
+ || /^\\\\/.test(target)
64
+ || /^\/(?!\/)/.test(target);
65
+ }
66
+
41
67
  function isFenceCloser(line: string, fence: FenceState): boolean {
42
68
  const match = /^[ \t]*(`+|~+)[ \t]*$/.exec(line);
43
69
  const delimiter = match?.[1];