pikiclaw 0.3.16 → 0.3.17

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
@@ -258,6 +258,16 @@ See also: [ARCHITECTURE.md](ARCHITECTURE.md) · [INTEGRATION.md](INTEGRATION.md)
258
258
 
259
259
  ---
260
260
 
261
+ ## Contributing
262
+
263
+ Contributions are welcome! Whether it's a bug fix, new feature, or documentation improvement — we appreciate your help.
264
+
265
+ - Read the **[Contributing Guide](CONTRIBUTING.md)** to get started
266
+ - Check out [`good first issue`](https://github.com/xiaotonng/pikiclaw/labels/good%20first%20issue) and [`help wanted`](https://github.com/xiaotonng/pikiclaw/labels/help%20wanted) labels for contribution ideas
267
+ - Open an issue first for larger changes so we can discuss the approach
268
+
269
+ ---
270
+
261
271
  ## License
262
272
 
263
273
  [MIT](LICENSE)
@@ -7,6 +7,36 @@
7
7
  import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
8
8
  import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './streaming.js';
9
9
  // ---------------------------------------------------------------------------
10
+ // GFM table parsing
11
+ // ---------------------------------------------------------------------------
12
+ /** Parse GFM table lines into structured headers + rows. */
13
+ export function parseGfmTable(tableLines) {
14
+ if (tableLines.length < 3)
15
+ return null;
16
+ const parseRow = (line) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
17
+ const isSep = (line) => {
18
+ const cells = parseRow(line);
19
+ return cells.length > 0 && cells.every(c => /^:?-{2,}:?$/.test(c));
20
+ };
21
+ let headerIdx = -1;
22
+ for (let i = 0; i < tableLines.length - 1; i++) {
23
+ if (isSep(tableLines[i + 1])) {
24
+ headerIdx = i;
25
+ break;
26
+ }
27
+ }
28
+ if (headerIdx < 0)
29
+ return null;
30
+ const headers = parseRow(tableLines[headerIdx]);
31
+ const rows = [];
32
+ for (let i = headerIdx + 2; i < tableLines.length; i++) {
33
+ if (isSep(tableLines[i]))
34
+ continue;
35
+ rows.push(parseRow(tableLines[i]));
36
+ }
37
+ return rows.length ? { headers, rows } : null;
38
+ }
39
+ // ---------------------------------------------------------------------------
10
40
  // Footer helpers
11
41
  // ---------------------------------------------------------------------------
12
42
  export function fmtCompactUptime(ms) {
@@ -122,28 +122,42 @@ function buildCardFromView(view) {
122
122
  const content = adapted.length > FEISHU_CARD_MAX
123
123
  ? adapted.slice(0, FEISHU_CARD_MAX) + '\n\n...(truncated)'
124
124
  : adapted;
125
- const card = {
126
- config: { wide_screen_mode: true, update_multi: true },
127
- elements: [{ tag: 'markdown', content }],
128
- };
129
- if (view.title) {
130
- card.header = {
131
- template: view.template || 'blue',
132
- title: { content: view.title, tag: 'plain_text' },
133
- };
134
- }
125
+ const actionElements = [];
135
126
  for (const row of view.rows || []) {
136
127
  const actions = row.actions.filter(Boolean);
137
128
  if (!actions.length)
138
129
  continue;
139
- const element = {
140
- tag: 'action',
141
- actions,
142
- };
130
+ const element = { tag: 'action', actions };
143
131
  const layout = row.layout || inferActionLayout(actions);
144
132
  if (layout)
145
133
  element.layout = layout;
146
- card.elements.push(element);
134
+ actionElements.push(element);
135
+ }
136
+ // Card JSON 2.0 supports tables in markdown but dropped `tag: action`.
137
+ // Use v2 for content-only cards; fall back to v1 when buttons are needed.
138
+ if (actionElements.length) {
139
+ const card = {
140
+ config: { wide_screen_mode: true, update_multi: true },
141
+ elements: [{ tag: 'markdown', content }, ...actionElements],
142
+ };
143
+ if (view.title) {
144
+ card.header = {
145
+ template: view.template || 'blue',
146
+ title: { content: view.title, tag: 'plain_text' },
147
+ };
148
+ }
149
+ return card;
150
+ }
151
+ const card = {
152
+ schema: '2.0',
153
+ config: { update_multi: true },
154
+ body: { elements: [{ tag: 'markdown', content }] },
155
+ };
156
+ if (view.title) {
157
+ card.header = {
158
+ template: view.template || 'blue',
159
+ title: { content: view.title, tag: 'plain_text' },
160
+ };
147
161
  }
148
162
  return card;
149
163
  }
@@ -74,7 +74,7 @@ export function adaptMarkdownForFeishu(markdown) {
74
74
  i++;
75
75
  continue;
76
76
  }
77
- // Pass GFM tables through — Feishu card markdown supports tables natively
77
+ // Pass GFM tables through — rendered natively with card schema 2.0
78
78
  if (i + 1 < lines.length && isGfmTableRow(lines[i]) && isGfmTableSeparator(lines[i + 1])) {
79
79
  while (i < lines.length && isGfmTableRow(lines[i])) {
80
80
  out.push(lines[i]);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { encodeCommandAction } from '../../bot/command-ui.js';
5
5
  import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from '../../bot/human-loop.js';
6
- import { footerStatusSymbol, formatFooterSummary, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, } from '../../bot/render-shared.js';
6
+ import { footerStatusSymbol, formatFooterSummary, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, parseGfmTable, } from '../../bot/render-shared.js';
7
7
  export function escapeHtml(t) {
8
8
  return t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
9
9
  }
@@ -194,15 +194,28 @@ export function mdToTgHtml(text) {
194
194
  const tableLine = lines[i].trim();
195
195
  if (!tableLine.startsWith('|'))
196
196
  break;
197
- if (/^\|[\s\-:|]+\|$/.test(tableLine)) {
198
- i++;
199
- continue;
200
- }
201
197
  tableLines.push(tableLine);
202
198
  i++;
203
199
  }
204
- if (tableLines.length)
205
- result.push(`<pre>${escapeHtml(tableLines.join('\n'))}</pre>`);
200
+ const parsed = parseGfmTable(tableLines);
201
+ if (parsed && parsed.headers.length >= 2) {
202
+ const parts = [];
203
+ for (let r = 0; r < parsed.rows.length; r++) {
204
+ if (r > 0)
205
+ parts.push('');
206
+ const cells = parsed.rows[r];
207
+ parts.push(`<b>${escapeHtml(cells[0] || '')}</b>`);
208
+ for (let c = 1; c < parsed.headers.length; c++) {
209
+ parts.push(`${escapeHtml(parsed.headers[c])}: ${escapeHtml(cells[c] || '')}`);
210
+ }
211
+ }
212
+ result.push(parts.join('\n'));
213
+ }
214
+ else {
215
+ const plain = tableLines.filter(l => !/^\|[\s\-:|]+\|$/.test(l.trim()));
216
+ if (plain.length)
217
+ result.push(`<pre>${escapeHtml(plain.join('\n'))}</pre>`);
218
+ }
206
219
  continue;
207
220
  }
208
221
  const heading = line.match(/^(#{1,6})\s+(.+)$/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {