@gobing-ai/knowledge-kit 0.0.31 → 0.0.32
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/dist/index.js +10 -10
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +9 -9
- package/plugins/generations/core-facts-gen/dist/index.js +12 -12
- package/plugins/generations/daily-article-gen/dist/index.js +12 -12
- package/plugins/generations/dailynews-gen/dist/index.js +9 -9
- package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
- package/plugins/generations/gemini-tts/dist/index.js +9 -9
- package/plugins/generations/image-gen/dist/index.js +9 -9
- package/plugins/generations/news-report-gen/dist/index.js +12 -12
- package/plugins/generations/voice-gen/dist/index.js +9 -9
- package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
- package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
- package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
- package/plugins/ingestions/web-search/dist/index.js +10 -10
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +173 -15
- package/plugins/kk/skills/content-judge/SKILL.md +12 -2
- package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
- package/plugins/kk/skills/content-safety/SKILL.md +24 -13
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +65 -8
- package/plugins/publishings/emdash-pub/dist/index.js +9 -9
- package/plugins/publishings/infoq-pub/dist/index.js +8602 -211
- package/plugins/publishings/infoq-pub/package.json +3 -1
- package/plugins/publishings/infoq-pub/plugin.json +1 -1
- package/plugins/publishings/infoq-pub/src/index.ts +246 -29
- package/plugins/publishings/infoq-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/juejin-pub/dist/index.js +8665 -211
- package/plugins/publishings/juejin-pub/package.json +3 -1
- package/plugins/publishings/juejin-pub/plugin.json +1 -1
- package/plugins/publishings/juejin-pub/src/index.ts +305 -29
- package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/medium-pub/dist/index.js +9 -9
- package/plugins/publishings/podcast-pub/dist/index.js +9 -9
- package/plugins/publishings/qiita-pub/dist/index.js +9 -9
- package/plugins/publishings/substack-pub/dist/index.js +8610 -211
- package/plugins/publishings/substack-pub/package.json +3 -1
- package/plugins/publishings/substack-pub/plugin.json +1 -1
- package/plugins/publishings/substack-pub/src/index.ts +271 -29
- package/plugins/publishings/substack-pub/src/profile-check.ts +74 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
- package/plugins/publishings/wechatmp-pub/dist/index.js +1227 -348
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
- package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +186 -516
- package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
- package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -5
- package/plugins/publishings/x-pub/dist/index.js +8626 -221
- package/plugins/publishings/x-pub/package.json +3 -1
- package/plugins/publishings/x-pub/plugin.json +1 -1
- package/plugins/publishings/x-pub/src/index.ts +258 -20
- package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/xhs-pub/dist/index.js +843 -346
- package/plugins/publishings/xhs-pub/plugin.json +1 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
- package/plugins/publishings/xhs-pub/src/index.ts +101 -593
- package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
- package/plugins/publishings/xhs-pub/src/page.ts +267 -0
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -3
- package/plugins/publishings/zenn-pub/dist/index.js +9 -9
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-browser-automation": "catalog:",
|
|
10
11
|
"@gobing-ai/publish-harness": "workspace:*",
|
|
11
12
|
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
-
"@gobing-ai/ts-utils": "catalog:"
|
|
13
|
+
"@gobing-ai/ts-utils": "catalog:",
|
|
14
|
+
"playwright": "catalog:"
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"@types/bun": "1.3.14"
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"kind": "publish",
|
|
4
4
|
"entry": "./dist/index.js",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "post via official tweets API; article via script browser transport (ts-browser-automation)"
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
2
|
+
import { dirname, extname, isAbsolute } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
5
5
|
import {
|
|
@@ -8,10 +8,28 @@ import {
|
|
|
8
8
|
FakeFileCliTransport,
|
|
9
9
|
type PublishTransport,
|
|
10
10
|
type PublishTransportPayload,
|
|
11
|
-
|
|
11
|
+
trySelectors,
|
|
12
12
|
} from '@gobing-ai/publish-harness';
|
|
13
|
+
import {
|
|
14
|
+
type ChannelBrowserConfig,
|
|
15
|
+
checkChannelSession,
|
|
16
|
+
expectOutcome,
|
|
17
|
+
markdownToHtml,
|
|
18
|
+
pasteHtml,
|
|
19
|
+
resolveChannelProfileDir,
|
|
20
|
+
resolveSelector,
|
|
21
|
+
uploadFile,
|
|
22
|
+
} from '@gobing-ai/publish-harness/browser';
|
|
23
|
+
import {
|
|
24
|
+
type BrowserLauncher,
|
|
25
|
+
BrowserProfileMissingError,
|
|
26
|
+
type IsAuthenticatedPredicate,
|
|
27
|
+
LoginTimeoutError,
|
|
28
|
+
withProfilePage,
|
|
29
|
+
} from '@gobing-ai/ts-browser-automation';
|
|
13
30
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
14
31
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
32
|
+
import type { Page } from 'playwright';
|
|
15
33
|
|
|
16
34
|
/** Per-request timeout (ms) for the X API call — Bun's `fetch` has no default deadline (R5). */
|
|
17
35
|
const REQUEST_TIMEOUT_MS = 120_000;
|
|
@@ -127,43 +145,263 @@ export class XPostApiTransport implements PublishTransport {
|
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
/** Channel slug for the article browser session: resolves the `X_PROFILE_DIR` tier (unchanged). */
|
|
149
|
+
export const ARTICLE_CHANNEL = 'x';
|
|
150
|
+
/** X's Articles composer; the preflight and the editor visit both open it. */
|
|
151
|
+
export const X_ARTICLES_URL = 'https://x.com/compose/articles';
|
|
152
|
+
/** The draft signal: the editor URL X renders once the article exists — the only draft evidence. */
|
|
153
|
+
export const ARTICLE_EDIT_RE = /\/compose\/articles\/edit\/(\d+)/;
|
|
154
|
+
/** Login/redirect URLs that mean "not authenticated" — the composer never renders behind them. */
|
|
155
|
+
export const LOGIN_URL_RE = /(\/i\/flow\/login|\/login)/;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Selectors seeded from the wt script (`x-article-playwright.ts:143-187`); each list is tried in
|
|
159
|
+
* order and the operator dogfood (0194) re-measures them. The seed's `tweetButton`/`Tweet`
|
|
160
|
+
* candidates are dropped: they belong to the tweet composer, and clicking one would post a tweet.
|
|
161
|
+
*/
|
|
162
|
+
export const ARTICLE_SELECTORS = {
|
|
163
|
+
write: ['[data-testid="empty_state_button_text"]', 'button:has-text("Write")', '[aria-label*="Write" i]'],
|
|
164
|
+
title: ['textarea[placeholder="Add a title"]', '[data-testid="twitter-article-title"]'],
|
|
165
|
+
body: ['.DraftEditor-editorContainer [contenteditable="true"]', 'div[role="textbox"]'],
|
|
166
|
+
addPhotos: ['[data-testid="addPhotoButton"]', 'button[aria-label*="Add photos" i]'],
|
|
167
|
+
fileInput: ['[data-testid="fileInput"]', 'input[type="file"][accept*="image"]'],
|
|
168
|
+
publish: ['[data-testid="publishButton"]', 'button:has-text("Publish")'],
|
|
169
|
+
} as const;
|
|
170
|
+
|
|
171
|
+
const STEP_TIMEOUT_MS = 30_000;
|
|
172
|
+
const LOGIN_TIMEOUT_MS = 180_000;
|
|
173
|
+
/** Operator recovery command every session failure names — never a cookie, token, page HTML or stack. */
|
|
174
|
+
const LOGIN_INSTRUCTION = 'bun plugins/publishings/x-pub/src/profile-check.ts login';
|
|
175
|
+
/** A live article lands on a URL carrying the article id; the composer URL is `/edit/`. */
|
|
176
|
+
const LIVE_URL_ID_RE = /\/articles?\/(\d+)/;
|
|
177
|
+
/** Cover extensions accepted from `options.coverPath` / `metadata.coverImage` (R2). */
|
|
178
|
+
const COVER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* X-owned readiness predicate: no login redirect and the article composer reachable (the write
|
|
182
|
+
* control or the title field attached). Shared by the publish preflight and the profile CLI (R1, R5).
|
|
183
|
+
*/
|
|
184
|
+
export const isArticleAuthenticated: IsAuthenticatedPredicate = async (page) => {
|
|
185
|
+
if (LOGIN_URL_RE.test(page.url())) return false;
|
|
186
|
+
if ((await trySelectors(page, ARTICLE_SELECTORS.write, { visible: false })).found) return true;
|
|
187
|
+
return (await trySelectors(page, ARTICLE_SELECTORS.title, { visible: false })).found;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Site-owned config for the shared session check and operator CLI (0183). */
|
|
191
|
+
export const ARTICLE_CONFIG: ChannelBrowserConfig = {
|
|
192
|
+
channel: ARTICLE_CHANNEL,
|
|
193
|
+
homeUrl: X_ARTICLES_URL,
|
|
194
|
+
isAuthenticated: isArticleAuthenticated,
|
|
195
|
+
loginTimeoutMs: LOGIN_TIMEOUT_MS,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function errorMessage(error: unknown): string {
|
|
199
|
+
return error instanceof Error ? error.message : String(error);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Session failures whose one-liner must name the recovery command (R5). */
|
|
203
|
+
function needsLogin(error: unknown): boolean {
|
|
204
|
+
return error instanceof LoginTimeoutError || error instanceof BrowserProfileMissingError;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolves and validates the optional cover (R2): `options.coverPath` wins over
|
|
209
|
+
* `metadata.coverImage`, and a given cover must be an absolute path to an existing
|
|
210
|
+
* `.png|.jpg|.jpeg|.webp` file. Returns the failure message otherwise, so a bad cover is refused
|
|
211
|
+
* **before** any browser opens.
|
|
212
|
+
*/
|
|
213
|
+
async function resolveCover(
|
|
214
|
+
options: Record<string, unknown> | undefined,
|
|
215
|
+
content: Content,
|
|
216
|
+
): Promise<{ path: string } | { error: string } | undefined> {
|
|
217
|
+
const raw = options?.coverPath ?? content.metadata?.coverImage;
|
|
218
|
+
if (raw === undefined) return undefined;
|
|
219
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
220
|
+
return { error: `cover path must be a non-empty local file path, got '${String(raw)}'` };
|
|
221
|
+
}
|
|
222
|
+
if (!isAbsolute(raw)) {
|
|
223
|
+
return { error: `cover path must be an absolute path, got '${raw}'` };
|
|
224
|
+
}
|
|
225
|
+
if (!COVER_EXTS.has(extname(raw).toLowerCase())) {
|
|
226
|
+
return { error: `cover path must be a .png, .jpg, .jpeg or .webp file, got '${raw}'` };
|
|
227
|
+
}
|
|
228
|
+
if (!(await createNodeFileSystem().exists(raw))) {
|
|
229
|
+
return { error: `cover file does not exist: '${raw}'` };
|
|
230
|
+
}
|
|
231
|
+
return { path: raw };
|
|
232
|
+
}
|
|
233
|
+
|
|
130
234
|
export class XArticleBrowserTransport implements PublishTransport {
|
|
131
235
|
public readonly kind = 'browser';
|
|
132
236
|
public readonly driver: BrowserDriver;
|
|
133
|
-
private readonly
|
|
134
|
-
private readonly
|
|
237
|
+
private readonly launcher?: BrowserLauncher;
|
|
238
|
+
private readonly stepTimeoutMs: number;
|
|
135
239
|
|
|
136
|
-
constructor(driver: BrowserDriver = '
|
|
240
|
+
constructor(driver: BrowserDriver = 'script', deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number } = {}) {
|
|
137
241
|
this.driver = driver;
|
|
138
|
-
this.
|
|
139
|
-
this.
|
|
242
|
+
this.launcher = deps.launcher;
|
|
243
|
+
this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
|
|
140
244
|
}
|
|
141
245
|
|
|
142
246
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
143
|
-
|
|
144
|
-
'x
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
return fail(`
|
|
247
|
+
if (this.driver !== 'script') {
|
|
248
|
+
return fail(`unsupported driver '${this.driver}' for x article`);
|
|
249
|
+
}
|
|
250
|
+
const requested = payload.content.metadata?.publishStatus;
|
|
251
|
+
if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
|
|
252
|
+
return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
|
|
253
|
+
}
|
|
254
|
+
// Pre-validation, before any browser opens: a given cover must be a usable local image (R2),
|
|
255
|
+
// so a bad path is a fast refusal rather than a half-built draft.
|
|
256
|
+
const cover = await resolveCover(payload.options, payload.content);
|
|
257
|
+
if (cover !== undefined && 'error' in cover) return fail(cover.error);
|
|
258
|
+
|
|
259
|
+
const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
|
|
260
|
+
const profileDir = resolveChannelProfileDir(ARTICLE_CHANNEL, override);
|
|
261
|
+
|
|
262
|
+
// ADR-025 preflight: an unauthenticated or absent profile must never produce a headed
|
|
263
|
+
// visit, so the read-only headless check runs first and a refusal stays a fast refusal.
|
|
264
|
+
const session = await checkChannelSession(ARTICLE_CONFIG, {
|
|
265
|
+
profileDir,
|
|
266
|
+
launcher: this.launcher,
|
|
267
|
+
headedFallback: false,
|
|
268
|
+
timeoutMs: this.stepTimeoutMs,
|
|
269
|
+
});
|
|
270
|
+
if (session.status !== 'verified') {
|
|
271
|
+
return fail(`${TARGET} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const publishStatus = requested === 'public' ? 'public' : 'draft';
|
|
275
|
+
try {
|
|
276
|
+
return await withProfilePage(
|
|
277
|
+
{
|
|
278
|
+
profileDir,
|
|
279
|
+
url: X_ARTICLES_URL,
|
|
280
|
+
isAuthenticated: isArticleAuthenticated,
|
|
281
|
+
// Headed by default; the headless preflight above is the unattended pass,
|
|
282
|
+
// `options.headless === true` the opt-in.
|
|
283
|
+
headless: payload.options?.headless === true,
|
|
284
|
+
launcher: this.launcher,
|
|
285
|
+
// The session is already preflight-verified, so the post-goto confirmation is
|
|
286
|
+
// a bounded step like every other one, not the 180s interactive login wait.
|
|
287
|
+
timeoutMs: this.stepTimeoutMs,
|
|
288
|
+
},
|
|
289
|
+
async ({ page }) => this.runArticleFlow(page, payload.content, publishStatus, cover?.path, profileDir),
|
|
290
|
+
);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const hint = needsLogin(error) ? ` — run: ${LOGIN_INSTRUCTION}` : '';
|
|
293
|
+
return fail(`${errorMessage(error)}${hint}`);
|
|
149
294
|
}
|
|
150
|
-
|
|
151
|
-
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The site-owned composer flow: write → title → Draft.js HTML paste → optional cover → the
|
|
299
|
+
* draft signal, then — for a live run only — the publish control and the live signal. Success
|
|
300
|
+
* only from an `expectOutcome` probe (E91-D6): a wait without an observed signal is never a
|
|
301
|
+
* success, and a draft run never clicks the publish control (E91-D5).
|
|
302
|
+
*/
|
|
303
|
+
private async runArticleFlow(
|
|
304
|
+
page: Page,
|
|
305
|
+
content: Content,
|
|
306
|
+
publishStatus: 'draft' | 'public',
|
|
307
|
+
coverPath: string | undefined,
|
|
308
|
+
profileDir: string,
|
|
309
|
+
): Promise<Result> {
|
|
310
|
+
const metadata = { operation: 'article', driver: this.driver, publishStatus, profileDir };
|
|
311
|
+
|
|
312
|
+
const writeSelector = await resolveSelector(page, ARTICLE_SELECTORS.write, {
|
|
313
|
+
timeoutMs: this.stepTimeoutMs,
|
|
314
|
+
label: 'write control',
|
|
315
|
+
});
|
|
316
|
+
await page.locator(writeSelector).first().click();
|
|
317
|
+
|
|
318
|
+
const titleSelector = await resolveSelector(page, ARTICLE_SELECTORS.title, {
|
|
319
|
+
timeoutMs: this.stepTimeoutMs,
|
|
320
|
+
label: 'title input',
|
|
321
|
+
});
|
|
322
|
+
await page
|
|
323
|
+
.locator(titleSelector)
|
|
324
|
+
.first()
|
|
325
|
+
.fill(content.title ?? 'Untitled');
|
|
326
|
+
|
|
327
|
+
const bodySelector = await resolveSelector(page, ARTICLE_SELECTORS.body, {
|
|
328
|
+
timeoutMs: this.stepTimeoutMs,
|
|
329
|
+
label: 'body editor',
|
|
330
|
+
});
|
|
331
|
+
// X's article editor is Draft.js rich text: the body goes in as rendered HTML through the
|
|
332
|
+
// shared paste, never as plain markdown text (E91-D4).
|
|
333
|
+
await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
|
|
334
|
+
|
|
335
|
+
if (coverPath !== undefined) {
|
|
336
|
+
// The photo picker is opened by a control when the composer renders it; the file input
|
|
337
|
+
// itself is routinely hidden behind it, so it is found by attachment (R2).
|
|
338
|
+
const addPhotos = await trySelectors(page, ARTICLE_SELECTORS.addPhotos, { visible: false });
|
|
339
|
+
if (addPhotos.found && addPhotos.selector !== null) {
|
|
340
|
+
await page.locator(addPhotos.selector).first().click();
|
|
341
|
+
}
|
|
342
|
+
const fileInputSelector = await resolveSelector(page, ARTICLE_SELECTORS.fileInput, {
|
|
343
|
+
timeoutMs: this.stepTimeoutMs,
|
|
344
|
+
label: 'cover file input',
|
|
345
|
+
});
|
|
346
|
+
await uploadFile(page, fileInputSelector, coverPath, { timeoutMs: this.stepTimeoutMs });
|
|
152
347
|
}
|
|
153
|
-
|
|
348
|
+
|
|
349
|
+
const draft = await expectOutcome(() => probeDraft(page), {
|
|
350
|
+
timeoutMs: this.stepTimeoutMs,
|
|
351
|
+
label: 'x article draft',
|
|
352
|
+
});
|
|
353
|
+
if (!draft.ok) return fail(draft.error);
|
|
354
|
+
|
|
355
|
+
// The probe only ever returns a `/compose/articles/edit/<id>` URL, so this always resolves.
|
|
356
|
+
const id = ARTICLE_EDIT_RE.exec(draft.evidence)?.[1];
|
|
357
|
+
if (publishStatus === 'draft') {
|
|
358
|
+
return ResultSchema.parse({
|
|
359
|
+
ok: true,
|
|
360
|
+
target: TARGET,
|
|
361
|
+
...(id === undefined ? {} : { id }),
|
|
362
|
+
url: draft.evidence,
|
|
363
|
+
message: `${TARGET} article draft saved (unpublished) in profile ${profileDir}`,
|
|
364
|
+
metadata,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const publishSelector = await resolveSelector(page, ARTICLE_SELECTORS.publish, {
|
|
369
|
+
timeoutMs: this.stepTimeoutMs,
|
|
370
|
+
label: 'publish control',
|
|
371
|
+
});
|
|
372
|
+
await page.locator(publishSelector).first().click();
|
|
373
|
+
const live = await expectOutcome(() => probeLive(page), {
|
|
374
|
+
timeoutMs: this.stepTimeoutMs,
|
|
375
|
+
label: `x article publish '${publishSelector}'`,
|
|
376
|
+
});
|
|
377
|
+
if (!live.ok) return fail(live.error);
|
|
378
|
+
|
|
154
379
|
return ResultSchema.parse({
|
|
155
380
|
ok: true,
|
|
156
381
|
target: TARGET,
|
|
157
|
-
id:
|
|
158
|
-
|
|
159
|
-
|
|
382
|
+
...(id === undefined ? {} : { id }),
|
|
383
|
+
url: live.evidence,
|
|
384
|
+
message: `${TARGET} article published live in profile ${profileDir}`,
|
|
385
|
+
metadata,
|
|
160
386
|
});
|
|
161
387
|
}
|
|
162
388
|
}
|
|
163
389
|
|
|
390
|
+
/** Draft evidence is URL-only by design: X autosaves the composer onto `/edit/<id>`. */
|
|
391
|
+
async function probeDraft(page: Page): Promise<string | null> {
|
|
392
|
+
const url = page.url();
|
|
393
|
+
return ARTICLE_EDIT_RE.test(url) ? url : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Live evidence is URL-only too: a published article URL that left the `/edit/` composer. */
|
|
397
|
+
async function probeLive(page: Page): Promise<string | null> {
|
|
398
|
+
const url = page.url();
|
|
399
|
+
return !ARTICLE_EDIT_RE.test(url) && LIVE_URL_ID_RE.test(url) ? url : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
164
402
|
export function getPublishTransport(operation: 'post' | 'article' = 'post'): PublishTransport {
|
|
165
403
|
if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
|
|
166
|
-
return operation === 'article' ? new FakeBrowserTransport('
|
|
404
|
+
return operation === 'article' ? new FakeBrowserTransport('script') : new FakeFileCliTransport();
|
|
167
405
|
}
|
|
168
406
|
return operation === 'article' ? new XArticleBrowserTransport() : new XPostApiTransport();
|
|
169
407
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X article profile-session capability (R5, AC3).
|
|
3
|
+
*
|
|
4
|
+
* `login` opens the Articles composer in a headed browser and resolves only once X's readiness
|
|
5
|
+
* predicate passes; `check` reopens the profile headlessly and reports `verified` or `refused`.
|
|
6
|
+
* Arg parsing, profile precedence (`--profile` > `X_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` >
|
|
7
|
+
* `~/.config/kk/browser-profiles/x`) and the exit codes (0 verified, 1 refused, 2 usage error) all
|
|
8
|
+
* live in the shared `runProfileCli` (0183), so the publish preflight and this CLI apply the same
|
|
9
|
+
* gate.
|
|
10
|
+
*
|
|
11
|
+
* Operator dogfood (plan step 6, handed to 0194): run `profile-check.ts login`, then `check`, on
|
|
12
|
+
* the same profile, and re-measure the selectors and the edit-URL pattern.
|
|
13
|
+
*/
|
|
14
|
+
import { runProfileCli } from '@gobing-ai/publish-harness/browser';
|
|
15
|
+
import { ARTICLE_CONFIG } from './index';
|
|
16
|
+
|
|
17
|
+
if (import.meta.main) {
|
|
18
|
+
process.exit(await runProfileCli(ARTICLE_CONFIG, Bun.argv.slice(2)));
|
|
19
|
+
}
|