typeclaw 0.31.1 → 0.32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -129,14 +129,34 @@ type PlatformInfo = {
129
129
  // the call would no-op. Keep in sync with the adapters that call
130
130
  // `router.registerReaction` (github, slack-bot, discord-bot today).
131
131
  supportsReactions: boolean
132
+ // Whether this adapter's OutboundCallback accepts file attachments. Gates the
133
+ // "ship a researcher report as a PDF by default" prompt guidance: a report is
134
+ // only worth converting to a downloadable file on channels that can actually
135
+ // receive one. GitHub's outbound callback hard-rejects attachments
136
+ // (`github-bot-does-not-support-attachments` in adapters/github/outbound.ts),
137
+ // so a PDF nudge there would train the model toward a call that always fails;
138
+ // the other four upload files (Slack `uploadFile`, Discord `uploadFile`,
139
+ // Telegram `sendDocument`, KakaoTalk `sendAttachment`). Keep in sync with the
140
+ // adapters' outbound callbacks.
141
+ supportsAttachments: boolean
132
142
  }
133
143
 
134
144
  const PLATFORM_INFO: Record<AdapterId, PlatformInfo> = {
135
- 'slack-bot': { displayName: 'Slack', mentionMode: 'angle-id', supportsReactions: true },
136
- 'discord-bot': { displayName: 'Discord', mentionMode: 'angle-id', supportsReactions: true },
137
- github: { displayName: 'GitHub', mentionMode: 'at-username', supportsReactions: true },
138
- 'telegram-bot': { displayName: 'Telegram', mentionMode: 'at-username', supportsReactions: false },
139
- kakaotalk: { displayName: 'KakaoTalk', mentionMode: 'alias', supportsReactions: false },
145
+ 'slack-bot': { displayName: 'Slack', mentionMode: 'angle-id', supportsReactions: true, supportsAttachments: true },
146
+ 'discord-bot': {
147
+ displayName: 'Discord',
148
+ mentionMode: 'angle-id',
149
+ supportsReactions: true,
150
+ supportsAttachments: true,
151
+ },
152
+ github: { displayName: 'GitHub', mentionMode: 'at-username', supportsReactions: true, supportsAttachments: false },
153
+ 'telegram-bot': {
154
+ displayName: 'Telegram',
155
+ mentionMode: 'at-username',
156
+ supportsReactions: false,
157
+ supportsAttachments: true,
158
+ },
159
+ kakaotalk: { displayName: 'KakaoTalk', mentionMode: 'alias', supportsReactions: false, supportsAttachments: true },
140
160
  }
141
161
 
142
162
  function getPlatformInfo(adapter: AdapterId): PlatformInfo {
@@ -461,6 +481,7 @@ function renderChannelOrigin(
461
481
  "matching the channel's `allow` rules are accepted (the tool returns",
462
482
  '`{ ok: false }` otherwise).',
463
483
  '',
484
+ ...renderResearchReportDeliveryGuidance(platformInfo),
464
485
  ...renderMentionGuidance(platformInfo, origin.participants ?? [], now, origin.self),
465
486
  )
466
487
 
@@ -496,6 +517,30 @@ function renderMembershipSummary(
496
517
  return `This channel has approximately ${total} members (about ${membership.humans} humans, ${membership.bots} bots — the bot count is approximate, the full member list was not enumerated because it exceeds the 50-member cap). The 10 most recent speakers are listed below.`
497
518
  }
498
519
 
520
+ // The `researcher` subagent always hands back a markdown report file
521
+ // (`research-<slug>.md`) and is itself read-only — it cannot produce the PDF.
522
+ // Whoever delivers that report to a channel is the one who decides the format,
523
+ // and on a channel that accepts file uploads the right default for a multi-page
524
+ // research report is a downloadable PDF, not a wall of raw markdown dumped into
525
+ // chat. This block makes that the default ONLY where it is actionable: gated on
526
+ // `supportsAttachments` so GitHub (whose outbound callback rejects attachments)
527
+ // never gets a nudge toward a `channel_send` call that would fail.
528
+ function renderResearchReportDeliveryGuidance(platformInfo: PlatformInfo): string[] {
529
+ if (!platformInfo.supportsAttachments) return []
530
+ return [
531
+ `**Ship \`researcher\` reports as a PDF by default.** ${platformInfo.displayName} accepts file`,
532
+ 'attachments. When you receive a `researcher` subagent result — a',
533
+ '`research-<slug>.md` report file path in its `<report>` block — convert that',
534
+ 'markdown to a PDF with the `typeclaw-markdown-pdf` skill and deliver it with',
535
+ '`channel_send({ ..., attachments: [{ path, filename }] })`, with a one- or',
536
+ 'two-line summary as the message text. A downloadable file is what a human',
537
+ 'wants for a multi-page report; do not paste the full markdown into chat. Send',
538
+ 'the report inline as plain text only if the caller explicitly asked for it in',
539
+ 'the message, or the report is short enough that a file would be overkill.',
540
+ '',
541
+ ]
542
+ }
543
+
499
544
  function renderMentionGuidance(
500
545
  platformInfo: PlatformInfo,
501
546
  participants: readonly ChannelParticipant[],
@@ -108,7 +108,7 @@ fonts/margins only if the user asks.
108
108
  #counter(page).display("1 / 1", both: true)
109
109
  ]),
110
110
  )
111
- #set text(font: ("Libertinus Serif", "New Computer Modern"), size: 11pt, lang: "en")
111
+ #set text(font: ("Libertinus Serif", "New Computer Modern", "Noto Serif CJK KR"), size: 11pt, lang: "en")
112
112
  #set par(justify: true, leading: 0.68em, spacing: 1.1em)
113
113
 
114
114
  #show heading: set text(weight: "semibold")
@@ -136,9 +136,15 @@ Notes:
136
136
  - `read("report.md")` is **relative to the workspace** (the compiler's `workspace`
137
137
  is set to `workspace/` — see Step 3). Keep the `.typ` and `.md` in `workspace/`.
138
138
  - Fonts `Libertinus Serif` / `New Computer Modern` are bundled with Typst (no font
139
- install). For Korean/CJK body text, add `"Noto Serif CJK KR"` to the `font:` list
140
- and pass that font dir to `fontPaths` in Step 3 (the container's `cjkFonts` toggle
141
- installs `fonts-noto-cjk` under `/usr/share/fonts`).
139
+ install) and carry the Latin text. `"Noto Serif CJK KR"` is appended as the
140
+ fallback so Korean/CJK glyphs resolve per-glyph Typst falls through to it
141
+ wherever the Latin fonts have no glyph, leaving Latin runs untouched. It comes
142
+ from `fonts-noto-cjk`, which Step 3's renderer loads from `/usr/share/fonts` via
143
+ `fontPaths`. **The package is only present when the container's `cjkFonts` toggle
144
+ resolves to `true`.** Its default is `"auto"`, which installs the fonts only when
145
+ the host locale is CJK (`ja`/`ko`/`zh`) — so on a non-CJK host, CJK PDFs still
146
+ render as tofu until you set `docker.file.cjkFonts: true` in `typeclaw.json` and
147
+ rebuild. If your CJK font lives elsewhere, add its dir to the `fontPaths` list.
142
148
 
143
149
  ## Step 3 — render
144
150
 
@@ -149,15 +155,23 @@ writes the PDF. Pass the wrapper and output paths as arguments.
149
155
  ```ts
150
156
  // workspace/.tools/render.ts
151
157
  import { NodeCompiler } from '@myriaddreamin/typst-ts-node-compiler'
152
- import { writeFileSync } from 'node:fs'
158
+ import { existsSync, writeFileSync } from 'node:fs'
153
159
 
154
160
  const [, , mainFile, outFile] = process.argv
155
161
  if (!mainFile || !outFile) throw new Error('usage: render.ts <main.typ> <out.pdf>')
156
162
 
163
+ // Load system fonts so CJK glyphs resolve. The compiler does NOT auto-discover
164
+ // system font dirs the way the Typst CLI does — without explicit fontPaths,
165
+ // "Noto Serif CJK KR" (from fonts-noto-cjk under /usr/share/fonts) is invisible
166
+ // and Korean/Japanese/Chinese text renders as .notdef tofu boxes. Filtered with
167
+ // existsSync so a missing dir (e.g. on a dev/host run) is skipped, not fatal.
168
+ const fontPaths = ['/usr/share/fonts', '/usr/local/share/fonts', '/Library/Fonts', '/System/Library/Fonts'].filter(
169
+ existsSync,
170
+ )
171
+
157
172
  const compiler = NodeCompiler.create({
158
173
  workspace: '.', // run from workspace/, so read("report.md") resolves
159
- // Add CJK / extra font dirs here if needed:
160
- // fontArgs: [{ fontPaths: ["/usr/share/fonts"] }],
174
+ ...(fontPaths.length > 0 ? { fontArgs: [{ fontPaths }] } : {}),
161
175
  })
162
176
  const pdf = compiler.pdf({ mainFilePath: mainFile })
163
177
  writeFileSync(outFile, Buffer.from(pdf))