larkway 0.3.23 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.23**
11
+ **Current release: v0.3.24**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.23**
11
+ **当前版本:v0.3.24**
12
12
 
13
13
  ---
14
14
 
package/dist/main.js CHANGED
@@ -117647,6 +117647,7 @@ function deriveTriggerFacts(parsed, isNewThread, larkCliProfile) {
117647
117647
  // src/agent/answerChannel.ts
117648
117648
  var ANSWER_BEGIN_MARKER = "LARKWAY_ANSWER_BEGIN";
117649
117649
  var ANSWER_END_MARKER = "LARKWAY_ANSWER_END";
117650
+ var STREAM_HOLD_CHARS = ANSWER_END_MARKER.length + 2;
117650
117651
  function stripLeadingNewline(text) {
117651
117652
  return text.replace(/^\r?\n/, "");
117652
117653
  }
@@ -117662,6 +117663,9 @@ function markerLineIndex(text, marker) {
117662
117663
  const after = lineEnd + (match[2]?.length ?? 0);
117663
117664
  return { start: lineStart, end: after };
117664
117665
  }
117666
+ function hasUsefulText(text) {
117667
+ return text.length > 0;
117668
+ }
117665
117669
  function splitAnswerChannelText(text, raw) {
117666
117670
  const begin = markerLineIndex(text, ANSWER_BEGIN_MARKER);
117667
117671
  if (!begin) return [{ type: "internal_text", text, raw }];
@@ -117676,6 +117680,71 @@ function splitAnswerChannelText(text, raw) {
117676
117680
  if (trailing.trim()) events.push({ type: "internal_text", text: trailing, raw });
117677
117681
  return events;
117678
117682
  }
117683
+ var AnswerChannelExtractor = class {
117684
+ mode = "waiting";
117685
+ buffer = "";
117686
+ visibleText = "";
117687
+ ingestDelta(text, raw) {
117688
+ if (!text || this.mode === "closed") return [];
117689
+ this.buffer += text;
117690
+ return this.drain(raw);
117691
+ }
117692
+ ingestSnapshot(text, raw) {
117693
+ const events = splitAnswerChannelText(text, raw);
117694
+ const out = [];
117695
+ for (const event of events) {
117696
+ if (event.type !== "answer_snapshot") {
117697
+ out.push(event);
117698
+ continue;
117699
+ }
117700
+ if (event.text === this.visibleText) continue;
117701
+ this.visibleText = event.text;
117702
+ out.push(event);
117703
+ if (text.includes(ANSWER_END_MARKER)) this.mode = "closed";
117704
+ }
117705
+ return out;
117706
+ }
117707
+ drain(raw) {
117708
+ const events = [];
117709
+ if (this.mode === "waiting") {
117710
+ const begin = markerLineIndex(this.buffer, ANSWER_BEGIN_MARKER);
117711
+ if (!begin) {
117712
+ this.trimWaitingBuffer();
117713
+ return events;
117714
+ }
117715
+ const before = stripTrailingNewline(this.buffer.slice(0, begin.start));
117716
+ if (before.trim()) events.push({ type: "internal_text", text: before, raw });
117717
+ this.buffer = this.buffer.slice(begin.end);
117718
+ this.mode = "answer";
117719
+ }
117720
+ if (this.mode !== "answer") return events;
117721
+ const end = markerLineIndex(this.buffer, ANSWER_END_MARKER);
117722
+ if (end) {
117723
+ const answerTail = stripTrailingNewline(this.buffer.slice(0, end.start));
117724
+ if (hasUsefulText(answerTail)) events.push(this.answerDelta(answerTail, raw));
117725
+ const trailing = stripLeadingNewline(this.buffer.slice(end.end));
117726
+ if (trailing.trim()) events.push({ type: "internal_text", text: trailing, raw });
117727
+ this.buffer = "";
117728
+ this.mode = "closed";
117729
+ return events;
117730
+ }
117731
+ if (this.buffer.length <= STREAM_HOLD_CHARS) return events;
117732
+ const emitText = this.buffer.slice(0, this.buffer.length - STREAM_HOLD_CHARS);
117733
+ this.buffer = this.buffer.slice(this.buffer.length - STREAM_HOLD_CHARS);
117734
+ if (hasUsefulText(emitText)) events.push(this.answerDelta(emitText, raw));
117735
+ return events;
117736
+ }
117737
+ answerDelta(text, raw) {
117738
+ this.visibleText += text;
117739
+ return { type: "answer_delta", text, raw };
117740
+ }
117741
+ trimWaitingBuffer() {
117742
+ const max = ANSWER_BEGIN_MARKER.length + 2;
117743
+ if (this.buffer.length > max) {
117744
+ this.buffer = this.buffer.slice(this.buffer.length - max);
117745
+ }
117746
+ }
117747
+ };
117679
117748
 
117680
117749
  // src/claude/prompt.ts
117681
117750
  var MEMORY_CATEGORY_FILE_NAMES = [
@@ -117719,8 +117788,7 @@ function renderStateContract(stateFilePath) {
117719
117788
  "- status: in_progress / ready / failed(bridge \u552F\u4E00\u5F3A\u4F9D\u8D56\u7684\u5B57\u6BB5)",
117720
117789
  "- last_message: \u7ED9\u8FD0\u8425\u770B\u7684\u5361\u7247\u6B63\u6587\u3002\u4F60\u81EA\u884C\u7EC4\u7EC7\u5C55\u793A\u5185\u5BB9,\u4F8B\u5982\u8FDB\u5EA6\u3001\u7ED3\u8BBA\u3001\u8BC1\u636E\u3001MR \u94FE\u63A5\u3001dev \u9884\u89C8\u3001\u9700\u8981\u7528\u6237\u8865\u5145\u7684\u4FE1\u606F",
117721
117790
  "- error: \u5931\u8D25\u539F\u56E0(\u642D\u914D status=failed)",
117722
- "- card_title: \u5361\u7247\u6807\u9898\u8986\u76D6(\u53EF\u9009)",
117723
- "- card_color: \u5361\u7247\u914D\u8272(\u53EF\u9009,success/failure/neutral,\u4E5F\u53EF\u76F4\u63A5\u5199 green/red/grey)",
117791
+ "- card_title/card_color: \u517C\u5BB9\u5B57\u6BB5; \u9ED8\u8BA4 CardKit \u4E0D\u6E32\u67D3\u9876\u90E8\u6807\u9898\u8272\u6761,legacy/fallback \u5361\u7247\u8DEF\u5F84\u53EF\u80FD\u4F7F\u7528",
117724
117792
  "- image_blocks: \u53EF\u9009\u56FE\u7247\u9884\u89C8\u5757\u6570\u7EC4,\u6700\u591A 4 \u4E2A\u3002\u6BCF\u9879 `{img_key, alt?, title?, mode?, preview?}`; `img_key` \u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20/\u53EF\u7528\u4E8E\u5361\u7247\u7684 Feishu \u56FE\u7247 key,`alt` \u7701\u7565\u65F6 bridge \u9ED8\u8BA4\u201C\u56FE\u7247\u9884\u89C8\u201D,`mode` \u53EA\u5141\u8BB8 `crop_center`/`fit_horizontal` \u5E76\u6620\u5C04\u5230 Card JSON 2.0 `scale_type`,`preview` \u9ED8\u8BA4 true\u3002bridge \u4E0D\u8D1F\u8D23\u4E0B\u8F7D/\u4E0A\u4F20/\u9009\u62E9\u56FE\u7247;\u8FD9\u4E9B\u7531\u4F60\u7528 lark-cli \u7B49\u5DE5\u5177\u5148\u5B8C\u6210\u3002",
117725
117793
  '- content_blocks: \u53EF\u9009\u6709\u5E8F\u6B63\u6587\u5757\u6570\u7EC4,\u6700\u591A 12 \u4E2A block\u3001\u6700\u591A 4 \u4E2A image block\u3002\u53EA\u652F\u6301\u7A84 union:`{type:"markdown", content}` \u548C `{type:"image", img_key, alt?, title?, mode?, preview?}`;\u4E0D\u652F\u6301 raw card JSON\u3002\u7528\u4E8E\u6B63\u6587\u4E0E\u56FE\u7247\u4EA4\u9519\u6392\u7248,\u4F8B\u5982 markdown -> image -> markdown -> image\u3002\u82E5 `content_blocks` \u975E\u7A7A,bridge \u4EE5\u5B83\u4F5C\u4E3A\u4E3B\u6B63\u6587\u5E76\u5FFD\u7565 `last_message` + `image_blocks` \u7684\u6B63\u6587\u6E32\u67D3,\u907F\u514D\u91CD\u590D;\u82E5\u7701\u7565\u5219\u4FDD\u6301\u65E7 `last_message` + `image_blocks` \u884C\u4E3A\u3002',
117726
117794
  '- response_surface: \u53EF\u9009\u8986\u76D6\u5B57\u6BB5,\u5F62\u5982 `{mode:"card"|"post"|"hybrid", primary?:"card"|"post", post?:{mentions:[{user_id,label?}]}, card?:{compact?:boolean, capabilities?:[...]}}`\u3002\u9ED8\u8BA4\u53EF\u4E0D\u5199;\u65E0\u663E\u5F0F card \u610F\u56FE\u65F6 bridge \u6309 CardKit \u6D41\u5F0F\u5361\u7247\u5904\u7406,\u6700\u7EC8\u6536\u6210\u5E72\u51C0\u603B\u7ED3\u5361\u3002\u9700\u8981\u771F\u5B9E @ \u65F6\u628A\u76EE\u6807\u5199\u5165 `post.mentions`;\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002',
@@ -119136,6 +119204,9 @@ function markdown(content, elementId) {
119136
119204
  if (elementId) element["element_id"] = elementId;
119137
119205
  return element;
119138
119206
  }
119207
+ function buildCardKitAnswerElement(content = "") {
119208
+ return markdown(content, CARDKIT_FINAL_ELEMENT_ID);
119209
+ }
119139
119210
  function safeAtMention(target) {
119140
119211
  const id = target.user_id.trim();
119141
119212
  if (!/^[A-Za-z0-9_:-]+$/.test(id)) return "";
@@ -119187,10 +119258,7 @@ function buildCardKitInitialCard(opts = {}) {
119187
119258
  }
119188
119259
  },
119189
119260
  body: {
119190
- elements: [
119191
- markdown(" ", CARDKIT_FINAL_ELEMENT_ID),
119192
- markdown(footerText, CARDKIT_FOOTER_ELEMENT_ID)
119193
- ]
119261
+ elements: [markdown(footerText, CARDKIT_FOOTER_ELEMENT_ID)]
119194
119262
  }
119195
119263
  };
119196
119264
  }
@@ -119237,7 +119305,7 @@ function finalMarkdown(opts) {
119237
119305
  return truncateChars([mentionLine, body].filter(Boolean).join("\n\n") || "\u5B8C\u6210\u3002", FINAL_BUDGET_CHARS, "\n\n_\u5185\u5BB9\u8FC7\u957F,\u5DF2\u622A\u65AD\u3002_");
119238
119306
  }
119239
119307
  function buildCardKitFinalCard(opts) {
119240
- const elements = [markdown(finalMarkdown(opts), CARDKIT_FINAL_ELEMENT_ID)];
119308
+ const elements = [buildCardKitAnswerElement(finalMarkdown(opts))];
119241
119309
  const images = [];
119242
119310
  if (opts.contentBlocks?.length) {
119243
119311
  for (const block of opts.contentBlocks) {
@@ -119263,10 +119331,6 @@ function buildCardKitFinalCard(opts) {
119263
119331
  streaming_mode: false,
119264
119332
  summary: { content: truncateChars(opts.finalText.replace(/\s+/g, " ").trim(), 50, "...") }
119265
119333
  },
119266
- header: {
119267
- title: plainText2(opts.title ?? "\u5B8C\u6210"),
119268
- template: "green"
119269
- },
119270
119334
  body: { elements }
119271
119335
  });
119272
119336
  }
@@ -119285,6 +119349,11 @@ function idempotencyKey(facts) {
119285
119349
  function sequenceUuid(cardId, role, sequence) {
119286
119350
  return deriveCardKitUuid([cardId, role, String(sequence)].join("\0"));
119287
119351
  }
119352
+ function isMissingCardKitElementError(err) {
119353
+ if (!(err instanceof Error)) return false;
119354
+ const message = err.message.toLowerCase();
119355
+ return message.includes("element") && (message.includes("not found") || message.includes("not exist") || message.includes("\u4E0D\u5B58\u5728"));
119356
+ }
119288
119357
  var LiveCardKitProgressHandle = class {
119289
119358
  cardId;
119290
119359
  messageId;
@@ -119298,6 +119367,7 @@ var LiveCardKitProgressHandle = class {
119298
119367
  inFlight = Promise.resolve();
119299
119368
  closed = false;
119300
119369
  progressUpdates = 0;
119370
+ answerElementCreated = false;
119301
119371
  sequence = 0;
119302
119372
  constructor(opts) {
119303
119373
  this.cardKitClient = opts.cardKitClient;
@@ -119336,6 +119406,7 @@ var LiveCardKitProgressHandle = class {
119336
119406
  this.closed = true;
119337
119407
  const finalMarkdown2 = buildCardKitFinalMarkdown(opts);
119338
119408
  if (finalMarkdown2 !== this.answerBuffer) {
119409
+ await this.withAnswerElement(finalMarkdown2);
119339
119410
  await this.next(
119340
119411
  (sequence) => this.cardKitClient.streamElementContent(
119341
119412
  this.cardId,
@@ -119391,17 +119462,35 @@ var LiveCardKitProgressHandle = class {
119391
119462
  if (!this.answerBuffer) return;
119392
119463
  this.progressUpdates += 1;
119393
119464
  this.inFlight = this.inFlight.then(
119394
- () => this.next(
119395
- (sequence) => this.cardKitClient.streamElementContent(this.cardId, CARDKIT_FINAL_ELEMENT_ID, this.answerBuffer, {
119396
- sequence,
119397
- uuid: sequenceUuid(this.cardId, "answer", sequence)
119398
- })
119465
+ () => this.withAnswerElement(this.answerBuffer).then(
119466
+ () => this.next(
119467
+ (sequence) => this.cardKitClient.streamElementContent(this.cardId, CARDKIT_FINAL_ELEMENT_ID, this.answerBuffer, {
119468
+ sequence,
119469
+ uuid: sequenceUuid(this.cardId, "answer", sequence)
119470
+ })
119471
+ )
119399
119472
  )
119400
119473
  ).catch((err) => {
119401
119474
  console.warn("[cardkit_progress] progress update failed (continuing):", err);
119402
119475
  });
119403
119476
  await this.inFlight;
119404
119477
  }
119478
+ async withAnswerElement(initialContent) {
119479
+ if (this.answerElementCreated) return;
119480
+ await this.next(
119481
+ (sequence) => this.cardKitClient.createElements(
119482
+ this.cardId,
119483
+ [buildCardKitAnswerElement(initialContent)],
119484
+ {
119485
+ sequence,
119486
+ uuid: sequenceUuid(this.cardId, "answer-element", sequence),
119487
+ type: "insert_before",
119488
+ targetElementId: CARDKIT_FOOTER_ELEMENT_ID
119489
+ }
119490
+ )
119491
+ );
119492
+ this.answerElementCreated = true;
119493
+ }
119405
119494
  async next(fn) {
119406
119495
  this.sequence += 1;
119407
119496
  await fn(this.sequence);
@@ -119451,7 +119540,7 @@ async function finalizeExistingCardKitCard(opts) {
119451
119540
  await opts.onSequenceCommitted?.(sequence);
119452
119541
  };
119453
119542
  const finalMarkdown2 = buildCardKitFinalMarkdown(opts.final);
119454
- await next(
119543
+ const streamFinalContent = () => next(
119455
119544
  "reconcile-final-content",
119456
119545
  (seq2, uuid) => opts.cardKitClient.streamElementContent(
119457
119546
  opts.cardId,
@@ -119460,6 +119549,25 @@ async function finalizeExistingCardKitCard(opts) {
119460
119549
  { sequence: seq2, uuid }
119461
119550
  )
119462
119551
  );
119552
+ try {
119553
+ await streamFinalContent();
119554
+ } catch (err) {
119555
+ if (!isMissingCardKitElementError(err)) throw err;
119556
+ await next(
119557
+ "reconcile-final-element",
119558
+ (seq2, uuid) => opts.cardKitClient.createElements(
119559
+ opts.cardId,
119560
+ [buildCardKitAnswerElement(finalMarkdown2)],
119561
+ {
119562
+ sequence: seq2,
119563
+ uuid,
119564
+ type: "insert_before",
119565
+ targetElementId: CARDKIT_FOOTER_ELEMENT_ID
119566
+ }
119567
+ )
119568
+ );
119569
+ await streamFinalContent();
119570
+ }
119463
119571
  await next(
119464
119572
  "reconcile-final-card",
119465
119573
  (seq2, uuid) => opts.cardKitClient.updateCardEntity(
@@ -125439,7 +125547,7 @@ function buildCommand(opts) {
125439
125547
  args.push("-p", opts.prompt);
125440
125548
  return [bin, args];
125441
125549
  }
125442
- function* parseLinesMulti(line) {
125550
+ function* parseLinesMulti(line, answerExtractor = new AnswerChannelExtractor()) {
125443
125551
  const trimmed = line.trim();
125444
125552
  if (trimmed === "") return;
125445
125553
  let obj;
@@ -125473,7 +125581,7 @@ function* parseLinesMulti(line) {
125473
125581
  if (typeof item !== "object" || item === null) continue;
125474
125582
  const block = item;
125475
125583
  if (block["type"] === "text" && typeof block["text"] === "string") {
125476
- yield* splitAnswerChannelText(block["text"], obj);
125584
+ yield* answerExtractor.ingestSnapshot(block["text"], obj);
125477
125585
  emitted = true;
125478
125586
  } else if (block["type"] === "tool_use") {
125479
125587
  yield {
@@ -125491,6 +125599,22 @@ function* parseLinesMulti(line) {
125491
125599
  yield { type: "raw", raw: obj };
125492
125600
  return;
125493
125601
  }
125602
+ if (eventType === "stream_event") {
125603
+ const event = record["event"];
125604
+ if (typeof event === "object" && event !== null) {
125605
+ const streamEvent = event;
125606
+ const delta = streamEvent["delta"];
125607
+ if (typeof delta === "object" && delta !== null) {
125608
+ const deltaRecord = delta;
125609
+ if (streamEvent["type"] === "content_block_delta" && deltaRecord["type"] === "text_delta" && typeof deltaRecord["text"] === "string") {
125610
+ yield* answerExtractor.ingestDelta(deltaRecord["text"], obj);
125611
+ return;
125612
+ }
125613
+ }
125614
+ }
125615
+ yield { type: "raw", raw: obj };
125616
+ return;
125617
+ }
125494
125618
  if (eventType === "user") {
125495
125619
  const message = record["message"];
125496
125620
  if (typeof message === "object" && message !== null && Array.isArray(message["content"])) {
@@ -125664,9 +125788,10 @@ stderr: ${stderr}` : "")
125664
125788
  // unblock the events loop so handler.ts can reach card.finalize().
125665
125789
  signal: rlAbortController.signal
125666
125790
  });
125791
+ const answerExtractor = new AnswerChannelExtractor();
125667
125792
  try {
125668
125793
  for await (const line of rl) {
125669
- for (const event of parseLinesMulti(line)) {
125794
+ for (const event of parseLinesMulti(line, answerExtractor)) {
125670
125795
  if (event.type === "system_init") {
125671
125796
  discoveredSessionId = event.sessionId;
125672
125797
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",