@gobing-ai/knowledge-kit 0.0.35 → 0.0.37

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 (124) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/board/collect/index.css +42 -0
  4. package/dist/board/collect/index.js +787 -0
  5. package/dist/board/daily-voice/index.css +13 -0
  6. package/dist/board/daily-voice/index.js +902 -0
  7. package/dist/board/itc/index.css +14 -0
  8. package/dist/board/itc/index.js +1026 -0
  9. package/dist/index.js +12996 -10476
  10. package/package.json +10 -9
  11. package/plugins/generations/content-gen/dist/index.js +1015 -10814
  12. package/plugins/generations/content-gen/package.json +1 -1
  13. package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
  14. package/plugins/generations/core-facts-gen/package.json +1 -1
  15. package/plugins/generations/daily-article-gen/dist/index.js +1004 -10803
  16. package/plugins/generations/daily-article-gen/package.json +1 -1
  17. package/plugins/generations/dailynews-gen/dist/index.js +1111 -10888
  18. package/plugins/generations/dailynews-gen/package.json +1 -1
  19. package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
  20. package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
  21. package/plugins/generations/episode-plan-gen/package.json +1 -1
  22. package/plugins/generations/gemini-tts/dist/index.js +1104 -10903
  23. package/plugins/generations/gemini-tts/package.json +1 -1
  24. package/plugins/generations/image-gen/dist/index.js +1239 -11035
  25. package/plugins/generations/image-gen/package.json +1 -1
  26. package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
  27. package/plugins/generations/news-report-gen/package.json +1 -1
  28. package/plugins/generations/voice-gen/dist/index.js +1062 -10861
  29. package/plugins/generations/voice-gen/package.json +1 -1
  30. package/plugins/generations/voxcpm-tts/README.md +72 -0
  31. package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
  32. package/plugins/generations/voxcpm-tts/package.json +16 -0
  33. package/plugins/generations/voxcpm-tts/plugin.json +6 -0
  34. package/plugins/generations/voxcpm-tts/profiles.json +10 -0
  35. package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
  36. package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
  37. package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
  38. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
  39. package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
  40. package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
  41. package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
  42. package/plugins/ingestions/aihot-ingest/package.json +1 -1
  43. package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
  44. package/plugins/ingestions/horizon-ingest/package.json +1 -1
  45. package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
  46. package/plugins/ingestions/karakeep-local/package.json +1 -1
  47. package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
  48. package/plugins/ingestions/last30days-ingest/package.json +1 -1
  49. package/plugins/ingestions/web-search/dist/index.js +1250 -11033
  50. package/plugins/ingestions/web-search/package.json +1 -1
  51. package/plugins/kk/README.md +29 -2
  52. package/plugins/kk/plugin.json +1 -1
  53. package/plugins/kk/schedules/cron.tmpl +9 -1
  54. package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
  55. package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
  56. package/plugins/kk/scripts/daily-stages.ts +428 -46
  57. package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
  58. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -146
  59. package/plugins/kk/skills/publish/SKILL.md +8 -4
  60. package/plugins/kk/workflows/feature-verification.yaml +93 -0
  61. package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
  62. package/plugins/kk/workflows/kk-collect.yaml +43 -11
  63. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +4 -2
  64. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +87 -16
  65. package/plugins/kk/workflows/kk-daily-safety.yaml +15 -2
  66. package/plugins/kk/workflows/kk-itc.yaml +8 -1
  67. package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
  68. package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
  69. package/plugins/publishings/emdash-pub/package.json +1 -1
  70. package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
  71. package/plugins/publishings/infoq-pub/dist/profile-check.js +1677 -11199
  72. package/plugins/publishings/infoq-pub/package.json +1 -1
  73. package/plugins/publishings/infoq-pub/plugin.json +5 -1
  74. package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
  75. package/plugins/publishings/juejin-pub/dist/profile-check.js +1677 -11199
  76. package/plugins/publishings/juejin-pub/package.json +1 -1
  77. package/plugins/publishings/juejin-pub/plugin.json +5 -1
  78. package/plugins/publishings/medium-pub/dist/index.js +978 -10805
  79. package/plugins/publishings/medium-pub/package.json +1 -1
  80. package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
  81. package/plugins/publishings/podcast-pub/package.json +1 -1
  82. package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
  83. package/plugins/publishings/qiita-pub/package.json +1 -1
  84. package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
  85. package/plugins/publishings/substack-pub/dist/profile-check.js +1698 -11200
  86. package/plugins/publishings/substack-pub/package.json +1 -1
  87. package/plugins/publishings/substack-pub/plugin.json +4 -1
  88. package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
  89. package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
  90. package/plugins/publishings/surfdash-pub/package.json +1 -1
  91. package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
  92. package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
  93. package/plugins/publishings/threads-pub/dist/index.js +12394 -0
  94. package/plugins/publishings/threads-pub/package.json +17 -0
  95. package/plugins/publishings/threads-pub/plugin.json +7 -0
  96. package/plugins/publishings/threads-pub/src/index.ts +340 -0
  97. package/plugins/publishings/threads-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/wechatmp-pub/dist/index.js +2508 -11695
  99. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +1656 -11188
  100. package/plugins/publishings/wechatmp-pub/package.json +1 -1
  101. package/plugins/publishings/wechatmp-pub/plugin.json +5 -1
  102. package/plugins/publishings/wechatmp-pub/src/enrich.ts +244 -71
  103. package/plugins/publishings/wechatmp-pub/src/flow.ts +412 -96
  104. package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
  105. package/plugins/publishings/wechatmp-pub/src/page.ts +16 -0
  106. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
  107. package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
  108. package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
  109. package/plugins/publishings/weibo-pub/package.json +19 -0
  110. package/plugins/publishings/weibo-pub/plugin.json +12 -0
  111. package/plugins/publishings/weibo-pub/src/index.ts +868 -0
  112. package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
  113. package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
  114. package/plugins/publishings/x-pub/dist/index.js +1948 -11440
  115. package/plugins/publishings/x-pub/dist/profile-check.js +1677 -11199
  116. package/plugins/publishings/x-pub/package.json +1 -1
  117. package/plugins/publishings/x-pub/plugin.json +5 -1
  118. package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
  119. package/plugins/publishings/xhs-pub/dist/profile-check.js +1697 -11212
  120. package/plugins/publishings/xhs-pub/package.json +1 -1
  121. package/plugins/publishings/xhs-pub/plugin.json +5 -1
  122. package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
  123. package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
  124. package/plugins/publishings/zenn-pub/package.json +1 -1
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@gobing-ai/threads-pub",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/publish-harness": "workspace:*",
11
+ "@gobing-ai/ts-runtime": "catalog:",
12
+ "@gobing-ai/ts-utils": "catalog:"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.4.3"
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "threads-pub",
3
+ "kind": "publish",
4
+ "entry": "./dist/index.js",
5
+ "version": "1.0.0",
6
+ "description": "post text via the official Threads two-step publishing API (no browser path)"
7
+ }
@@ -0,0 +1,340 @@
1
+ import { dirname } from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
4
+ import { FakeFileCliTransport, type PublishTransport, type PublishTransportPayload } from '@gobing-ai/publish-harness';
5
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
6
+ import { echoError } from '@gobing-ai/ts-utils';
7
+
8
+ /** Per-request timeout (ms) for the Threads Graph call — Bun's `fetch` has no default deadline. */
9
+ const REQUEST_TIMEOUT_MS = 120_000;
10
+
11
+ const TARGET = 'threads';
12
+ /** Meta's Threads Graph host, version-pinned like the rest of the publish channel plugins. */
13
+ export const THREADS_API_BASE = 'https://graph.threads.net/v1.0';
14
+ const TOKEN_ENVS = ['THREADS_ACCESS_TOKEN', 'THREADS_USER_ID'] as const;
15
+
16
+ /**
17
+ * Threads' own hard limit on a text post: 500 characters. Counted in code points, which is the
18
+ * closest readable match to Meta's "characters"; overlong text is refused, never truncated (R3).
19
+ */
20
+ const MAX_TEXT_CHARS = 500;
21
+
22
+ /** Declared operations (ADR-029 dispatch): threads-pub posts text only. */
23
+ export const THREADS_OPERATIONS = ['post'] as const;
24
+ export type ThreadsOperation = (typeof THREADS_OPERATIONS)[number];
25
+
26
+ /** Removes every configured credential — raw and percent-encoded — from a message (R4). */
27
+ function redact(message: string): string {
28
+ let out = message;
29
+ for (const key of TOKEN_ENVS) {
30
+ const value = process.env[key];
31
+ if (!value || value.trim().length === 0) continue;
32
+ for (const form of new Set([value, encodeURIComponent(value)])) {
33
+ out = out.split(form).join('***');
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /** Every refusal passes through `redact`, so no path can write a token into the Result (R4). */
40
+ function fail(error: string): Result {
41
+ return ResultSchema.parse({ ok: false, target: TARGET, error: redact(error) });
42
+ }
43
+
44
+ /** A bounded, single-line rendering of an operator-supplied value for a refusal message (R5). */
45
+ function bounded(value: unknown): string {
46
+ if (Array.isArray(value)) return `${value.length} entr${value.length === 1 ? 'y' : 'ies'}`;
47
+ return String(value).replace(/\s+/g, ' ').slice(0, 100);
48
+ }
49
+
50
+ /** Bounds a diagnostic detail to one short line, so a Result never carries a stack or a JSON blob. */
51
+ function boundedDetail(detail: string): string {
52
+ return detail.replace(/\s+/g, ' ').trim().slice(0, 300);
53
+ }
54
+
55
+ /**
56
+ * The deferred surfaces this channel refuses by name before any transport or browser resolves
57
+ * (R5, R11): threads-pub has no browser path, and Threads media needs a public image host.
58
+ */
59
+ function refuseUnsupportedOptions(options?: Record<string, unknown>, content?: Content): string | undefined {
60
+ const driver = options?.driver;
61
+ if (driver !== undefined) {
62
+ return `unsupported driver option '${bounded(driver)}' for threads-pub: threads-pub posts text only`;
63
+ }
64
+ const images = options?.images ?? content?.metadata?.images;
65
+ if (images !== undefined) {
66
+ return `unsupported images option (${bounded(images)}) for threads-pub: threads-pub posts text only`;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ /**
72
+ * Resolves the requested operation and refuses the deferred options in one place, so the caller
73
+ * can reject a request before any transport — real or fake — is constructed (R5, R11).
74
+ * @param options - Per-call publish options (`operation`, `driver`, `images`).
75
+ * @param content - Content whose `metadata.operation` is the fallback source.
76
+ * @returns The resolved `post` operation, or the refusal message.
77
+ */
78
+ export function resolveOperation(
79
+ options?: Record<string, unknown>,
80
+ content?: Content,
81
+ ): { operation: ThreadsOperation; error?: undefined } | { operation?: undefined; error: string } {
82
+ const raw = options?.operation ?? content?.metadata?.operation ?? 'post';
83
+ if (typeof raw !== 'string' || !(THREADS_OPERATIONS as readonly string[]).includes(raw)) {
84
+ return {
85
+ error: `unsupported operation '${String(raw)}' for threads-pub: expected one of ${THREADS_OPERATIONS.join(', ')}`,
86
+ };
87
+ }
88
+ const unsupported = refuseUnsupportedOptions(options, content);
89
+ if (unsupported !== undefined) return { error: unsupported };
90
+ return { operation: raw as ThreadsOperation };
91
+ }
92
+
93
+ /** Payload of a Graph error body; only `error.message` is ever echoed, bounded and redacted (R4). */
94
+ interface GraphErrorBody {
95
+ error?: { message?: unknown };
96
+ }
97
+
98
+ /** Bounded Graph `error.message` when the response carries one; never the raw response body (R4). */
99
+ async function graphMessage(response: Response): Promise<string> {
100
+ try {
101
+ const body = (await response.json()) as GraphErrorBody;
102
+ const message = body.error?.message;
103
+ if (typeof message === 'string' && message.trim().length > 0) return message.trim().slice(0, 200);
104
+ } catch {
105
+ // A non-JSON error body carries nothing this channel is allowed to echo (R4).
106
+ }
107
+ return response.statusText.trim() || 'no error message';
108
+ }
109
+
110
+ /**
111
+ * Accepts a permalink only when it is an HTTPS Threads URL carrying no token material: a
112
+ * best-effort lookup must never turn an arbitrary Graph string into the Result's `url` (R2).
113
+ */
114
+ function acceptPermalink(raw: unknown, token: string): string | undefined {
115
+ if (typeof raw !== 'string' || raw.length === 0) return undefined;
116
+ let url: URL;
117
+ try {
118
+ url = new URL(raw);
119
+ } catch {
120
+ return undefined;
121
+ }
122
+ if (url.protocol !== 'https:') return undefined;
123
+ if (!/(^|\.)threads\.(net|com)$/i.test(url.hostname)) return undefined;
124
+ if (token.length > 0 && raw.includes(token)) return undefined;
125
+ return raw;
126
+ }
127
+
128
+ /** A container or publish id, validated as a decimal string before it reaches a URL or Result. */
129
+ interface StepId {
130
+ id: string;
131
+ }
132
+
133
+ /**
134
+ * Threads text transport: POST a TEXT container, then POST it to `threads_publish`. No browser, no
135
+ * retry loop, no truncation — every refusal is a `Result` with `ok: false` (R2, R3, R4).
136
+ */
137
+ export class ThreadsPostApiTransport implements PublishTransport {
138
+ public readonly kind = 'file-cli';
139
+
140
+ /**
141
+ * Publishes `Content.body` (or `title`) as one Threads text post.
142
+ * @param payload - Target content plus per-call options.
143
+ * @returns The publish `Result`, with the published media id and a best-effort permalink.
144
+ */
145
+ public async publish(payload: PublishTransportPayload): Promise<Result> {
146
+ const unsupported = refuseUnsupportedOptions(payload.options, payload.content);
147
+ if (unsupported !== undefined) return fail(unsupported);
148
+
149
+ const missing = TOKEN_ENVS.filter((key) => {
150
+ const value = process.env[key];
151
+ return !value || value.trim().length === 0;
152
+ });
153
+ if (missing.length > 0) {
154
+ return fail(
155
+ `${missing.join(', ')} environment variable${missing.length > 1 ? 's are' : ' is'} required: Threads Graph API credentials. post does not fall back to a browser.`,
156
+ );
157
+ }
158
+
159
+ const text = payload.content.body.trim() || payload.content.title?.trim() || '';
160
+ if (text.length === 0) {
161
+ return fail('threads post requires Content.body or Content.title');
162
+ }
163
+ const length = [...text].length;
164
+ if (length > MAX_TEXT_CHARS) {
165
+ return fail(
166
+ `threads post text is ${length} code points, over the ${MAX_TEXT_CHARS}-character limit: threads-pub never truncates`,
167
+ );
168
+ }
169
+
170
+ const token = process.env.THREADS_ACCESS_TOKEN ?? '';
171
+ const userId = process.env.THREADS_USER_ID ?? '';
172
+ const container = await this.postForm('threads container', `${THREADS_API_BASE}/${userId}/threads`, {
173
+ media_type: 'TEXT',
174
+ text,
175
+ access_token: token,
176
+ });
177
+ if ('error' in container) return fail(container.error);
178
+
179
+ const published = await this.postForm(
180
+ 'threads_publish',
181
+ `${THREADS_API_BASE}/${userId}/threads_publish`,
182
+ { creation_id: container.id, access_token: token },
183
+ ` for container ${container.id}`,
184
+ );
185
+ if ('error' in published) return fail(published.error);
186
+
187
+ const permalink = await this.fetchPermalink(token, published.id);
188
+ return ResultSchema.parse({
189
+ ok: true,
190
+ target: TARGET,
191
+ id: published.id,
192
+ ...(permalink === undefined ? {} : { url: permalink }),
193
+ message: 'Posted via the Threads publishing API',
194
+ metadata: { operation: 'post' },
195
+ });
196
+ }
197
+
198
+ /** One form-encoded Graph POST; failures name the step, the created container and the status. */
199
+ private async postForm(
200
+ step: string,
201
+ url: string,
202
+ fields: Record<string, string>,
203
+ suffix = '',
204
+ ): Promise<StepId | { error: string }> {
205
+ let response: Response;
206
+ try {
207
+ response = await fetch(url, {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
210
+ body: new URLSearchParams(fields).toString(),
211
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
212
+ });
213
+ } catch (error: unknown) {
214
+ const detail =
215
+ error instanceof Error && error.name === 'TimeoutError'
216
+ ? ` (timeout after ${REQUEST_TIMEOUT_MS}ms)`
217
+ : '';
218
+ return { error: `threads ${step} request failed${suffix}: no HTTP response was received${detail}` };
219
+ }
220
+ if (!response.ok) {
221
+ return {
222
+ error: `threads ${step} request failed (HTTP ${response.status})${suffix}: ${await graphMessage(response)}`,
223
+ };
224
+ }
225
+ let body: unknown;
226
+ try {
227
+ body = await response.json();
228
+ } catch {
229
+ return { error: `threads ${step} response is not valid JSON${suffix}` };
230
+ }
231
+ const id = (body as { id?: unknown }).id;
232
+ if (typeof id !== 'string' || id.length === 0) {
233
+ return { error: `threads ${step} response is missing a string id${suffix}` };
234
+ }
235
+ if (!/^\d+$/.test(id)) {
236
+ return { error: `threads ${step} response id '${id}' is not a decimal string${suffix}` };
237
+ }
238
+ return { id };
239
+ }
240
+
241
+ /** Best-effort permalink lookup: any failure omits `url` and never flips `ok` (R2). */
242
+ private async fetchPermalink(token: string, id: string): Promise<string | undefined> {
243
+ try {
244
+ const response = await fetch(
245
+ `${THREADS_API_BASE}/${id}?fields=permalink&access_token=${encodeURIComponent(token)}`,
246
+ { method: 'GET', signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) },
247
+ );
248
+ if (!response.ok) return undefined;
249
+ const body = (await response.json()) as { permalink?: unknown };
250
+ return acceptPermalink(body.permalink, token);
251
+ } catch {
252
+ return undefined;
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Resolves the channel transport for the configured environment: the fake file-cli transport
259
+ * under `KNOWLEDGE_KIT_PUBLISH_TRANSPORT=fake`, otherwise the real Threads Graph transport (R6).
260
+ * @returns The publish transport for this channel.
261
+ */
262
+ export function getPublishTransport(): PublishTransport {
263
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') return new FakeFileCliTransport();
264
+ return new ThreadsPostApiTransport();
265
+ }
266
+
267
+ /** Writes the Result to `--out`, creating its directory when the caller named a nested path. */
268
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
269
+ const outDir = dirname(outputPath);
270
+ if (outDir && outDir !== '.') await createNodeFileSystem().ensureDir(outDir);
271
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
272
+ }
273
+
274
+ /**
275
+ * The plugin's `--in/--out` contract: validate the Content, resolve and refuse the operation and
276
+ * deferred options, then publish through the injected or configured transport (R5, R6).
277
+ * @param inputPath - Path to the Content JSON.
278
+ * @param outputPath - Path the Result JSON is written to, on success and on refusal.
279
+ * @param transport - Optional transport injection for tests and offline runs.
280
+ */
281
+ export async function processPublishIO(
282
+ inputPath: string,
283
+ outputPath: string,
284
+ transport?: PublishTransport,
285
+ ): Promise<void> {
286
+ await createNodeFileSystem().deleteFile(outputPath);
287
+ let content: Content;
288
+ let options: Record<string, unknown> | undefined;
289
+ try {
290
+ const raw = JSON.parse(await createNodeFileSystem().readFile(inputPath)) as Record<string, unknown>;
291
+ content = ContentSchema.parse(raw);
292
+ if (raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null) {
293
+ options = raw.options as Record<string, unknown>;
294
+ }
295
+ } catch (error: unknown) {
296
+ const message = redact(
297
+ `Invalid Content input: ${boundedDetail(error instanceof Error ? error.message : String(error))}`,
298
+ );
299
+ await writePublishResult(outputPath, fail(message));
300
+ throw new Error(message);
301
+ }
302
+ const mode = resolveOperation(options, content);
303
+ if (mode.error !== undefined) {
304
+ await writePublishResult(outputPath, fail(mode.error));
305
+ throw new Error(mode.error);
306
+ }
307
+ const resolved = transport ?? getPublishTransport();
308
+ const result = ResultSchema.parse(await resolved.publish({ content, options }));
309
+ await writePublishResult(outputPath, result);
310
+ if (!result.ok) throw new Error(result.error ?? 'Publish failed');
311
+ }
312
+
313
+ /**
314
+ * CLI entry point: `--in <content.json> --out <result.json>`.
315
+ * @returns Process exit code (0 published, 1 refused or invalid arguments).
316
+ */
317
+ export async function main(): Promise<number> {
318
+ let values: { in?: string; out?: string };
319
+ try {
320
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
321
+ } catch (error: unknown) {
322
+ echoError(`Invalid arguments: ${error instanceof Error ? error.message : String(error)}`);
323
+ return 1;
324
+ }
325
+ if (!values.in || !values.out) {
326
+ echoError('Missing required arguments: --in and --out');
327
+ return 1;
328
+ }
329
+ try {
330
+ await processPublishIO(values.in, values.out);
331
+ return 0;
332
+ } catch (error: unknown) {
333
+ echoError(`threads-pub failed: ${redact(error instanceof Error ? error.message : String(error))}`);
334
+ return 1;
335
+ }
336
+ }
337
+
338
+ if (import.meta.main) {
339
+ process.exit(await main());
340
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }