@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/dist/board/collect/index.css +42 -0
- package/dist/board/collect/index.js +787 -0
- package/dist/board/daily-voice/index.css +13 -0
- package/dist/board/daily-voice/index.js +902 -0
- package/dist/board/itc/index.css +14 -0
- package/dist/board/itc/index.js +1026 -0
- package/dist/index.js +12996 -10476
- package/package.json +10 -9
- package/plugins/generations/content-gen/dist/index.js +1015 -10814
- package/plugins/generations/content-gen/package.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
- package/plugins/generations/core-facts-gen/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +1004 -10803
- package/plugins/generations/daily-article-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/dist/index.js +1111 -10888
- package/plugins/generations/dailynews-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
- package/plugins/generations/episode-plan-gen/package.json +1 -1
- package/plugins/generations/gemini-tts/dist/index.js +1104 -10903
- package/plugins/generations/gemini-tts/package.json +1 -1
- package/plugins/generations/image-gen/dist/index.js +1239 -11035
- package/plugins/generations/image-gen/package.json +1 -1
- package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
- package/plugins/generations/news-report-gen/package.json +1 -1
- package/plugins/generations/voice-gen/dist/index.js +1062 -10861
- package/plugins/generations/voice-gen/package.json +1 -1
- package/plugins/generations/voxcpm-tts/README.md +72 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
- package/plugins/generations/voxcpm-tts/package.json +16 -0
- package/plugins/generations/voxcpm-tts/plugin.json +6 -0
- package/plugins/generations/voxcpm-tts/profiles.json +10 -0
- package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
- package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
- package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
- package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
- package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
- package/plugins/ingestions/aihot-ingest/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
- package/plugins/ingestions/horizon-ingest/package.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
- package/plugins/ingestions/karakeep-local/package.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
- package/plugins/ingestions/last30days-ingest/package.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +1250 -11033
- package/plugins/ingestions/web-search/package.json +1 -1
- package/plugins/kk/README.md +29 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +9 -1
- package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
- package/plugins/kk/scripts/daily-stages.ts +428 -46
- package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -146
- package/plugins/kk/skills/publish/SKILL.md +8 -4
- package/plugins/kk/workflows/feature-verification.yaml +93 -0
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
- package/plugins/kk/workflows/kk-collect.yaml +43 -11
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +4 -2
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +87 -16
- package/plugins/kk/workflows/kk-daily-safety.yaml +15 -2
- package/plugins/kk/workflows/kk-itc.yaml +8 -1
- package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
- package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
- package/plugins/publishings/emdash-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
- package/plugins/publishings/infoq-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/infoq-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/plugin.json +5 -1
- package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
- package/plugins/publishings/juejin-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/juejin-pub/package.json +1 -1
- package/plugins/publishings/juejin-pub/plugin.json +5 -1
- package/plugins/publishings/medium-pub/dist/index.js +978 -10805
- package/plugins/publishings/medium-pub/package.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
- package/plugins/publishings/podcast-pub/package.json +1 -1
- package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
- package/plugins/publishings/qiita-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
- package/plugins/publishings/substack-pub/dist/profile-check.js +1698 -11200
- package/plugins/publishings/substack-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/plugin.json +4 -1
- package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
- package/plugins/publishings/surfdash-pub/package.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
- package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
- package/plugins/publishings/threads-pub/dist/index.js +12394 -0
- package/plugins/publishings/threads-pub/package.json +17 -0
- package/plugins/publishings/threads-pub/plugin.json +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +340 -0
- package/plugins/publishings/threads-pub/tsconfig.json +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +2508 -11695
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +1656 -11188
- package/plugins/publishings/wechatmp-pub/package.json +1 -1
- package/plugins/publishings/wechatmp-pub/plugin.json +5 -1
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +244 -71
- package/plugins/publishings/wechatmp-pub/src/flow.ts +412 -96
- package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
- package/plugins/publishings/wechatmp-pub/src/page.ts +16 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
- package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
- package/plugins/publishings/weibo-pub/package.json +19 -0
- package/plugins/publishings/weibo-pub/plugin.json +12 -0
- package/plugins/publishings/weibo-pub/src/index.ts +868 -0
- package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
- package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +1948 -11440
- package/plugins/publishings/x-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/x-pub/package.json +1 -1
- package/plugins/publishings/x-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
- package/plugins/publishings/xhs-pub/dist/profile-check.js +1697 -11212
- package/plugins/publishings/xhs-pub/package.json +1 -1
- package/plugins/publishings/xhs-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
- package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
- 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,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
|
+
}
|