@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": "Publish plugin for Substack via browser
|
|
6
|
+
"description": "Publish plugin for Substack (substack.com): post drafts through the ProseMirror editor via script browser transport (ts-browser-automation)"
|
|
7
7
|
}
|
|
@@ -6,63 +6,305 @@ import {
|
|
|
6
6
|
FakeBrowserTransport,
|
|
7
7
|
type PublishTransport,
|
|
8
8
|
type PublishTransportPayload,
|
|
9
|
-
|
|
9
|
+
trySelectors,
|
|
10
10
|
} from '@gobing-ai/publish-harness';
|
|
11
|
+
import {
|
|
12
|
+
type ChannelBrowserConfig,
|
|
13
|
+
checkChannelSession,
|
|
14
|
+
expectOutcome,
|
|
15
|
+
markdownToHtml,
|
|
16
|
+
pasteHtml,
|
|
17
|
+
resolveChannelProfileDir,
|
|
18
|
+
resolveSelector,
|
|
19
|
+
} from '@gobing-ai/publish-harness/browser';
|
|
20
|
+
import {
|
|
21
|
+
type BrowserLauncher,
|
|
22
|
+
BrowserProfileMissingError,
|
|
23
|
+
type IsAuthenticatedPredicate,
|
|
24
|
+
LoginTimeoutError,
|
|
25
|
+
withProfilePage,
|
|
26
|
+
} from '@gobing-ai/ts-browser-automation';
|
|
11
27
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
12
28
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
29
|
+
import type { Page } from 'playwright';
|
|
13
30
|
|
|
14
|
-
|
|
31
|
+
/** Channel slug: names the Result target and resolves the `SUBSTACK_PROFILE_DIR` tier. */
|
|
32
|
+
export const CHANNEL = 'substack';
|
|
15
33
|
const TARGET = 'substack';
|
|
34
|
+
/** Login/account URLs that mean "not authenticated" — the editor never renders behind them. */
|
|
35
|
+
export const LOGIN_URL_RE = /(\/sign-in|substack\.com\/account\/login)/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Selectors seeded from the wt script (`substack-playwright.ts:84-136`); each list is tried in
|
|
39
|
+
* order and the operator dogfood (0193) re-measures them. `send` is Substack's
|
|
40
|
+
* `Send to everyone now` — it emails every subscriber — so both live controls (E91-D5) are
|
|
41
|
+
* reached only from the `public` branch, after the draft branch has already returned.
|
|
42
|
+
*/
|
|
43
|
+
export const SELECTORS = {
|
|
44
|
+
title: ['[data-testid="title-input"]', 'textarea[placeholder="Title"]', 'input[placeholder="Title"]'],
|
|
45
|
+
subtitle: ['[data-testid="subtitle-input"]', 'textarea[placeholder="Add a subtitle..."]'],
|
|
46
|
+
body: ['.ProseMirror[contenteditable="true"]'],
|
|
47
|
+
continue: ['[data-testid="continue-button"]', 'button:has-text("Continue")'],
|
|
48
|
+
send: [
|
|
49
|
+
'[data-testid="publish-button"]',
|
|
50
|
+
'button:has-text("Send to everyone now")',
|
|
51
|
+
'button:has-text("Publish now")',
|
|
52
|
+
],
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
const STEP_TIMEOUT_MS = 30_000;
|
|
56
|
+
const LOGIN_TIMEOUT_MS = 180_000;
|
|
57
|
+
/** Operator recovery command every session failure names — never a cookie, page HTML or stack. */
|
|
58
|
+
const LOGIN_INSTRUCTION = 'bun plugins/publishings/substack-pub/src/profile-check.ts login';
|
|
59
|
+
/** Substack autosaves and rewrites the editor URL onto `/publish/post/<id>`, the whole draft signal. */
|
|
60
|
+
const DRAFT_URL_RE = /\/publish\/post\/(\d+)/;
|
|
61
|
+
/** A live post lands on `/p/<slug>`; the publish queue is `/publish/posts/published`. */
|
|
62
|
+
const LIVE_URL_RE = /\/p\/[^/?#]+|\/publish\/posts\/published/;
|
|
63
|
+
/** Both places the publication URL can come from, named as one thing in every refusal. */
|
|
64
|
+
const PUBLICATION_URL_SOURCES = 'SUBSTACK_PUBLICATION_URL (or --publication <url>)';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The publication's new-post editor: `${base}/publish/post/new`, where the base is the
|
|
68
|
+
* publication URL without its trailing `/` or `/publish` (seed `substack-playwright.ts:237-244`).
|
|
69
|
+
*/
|
|
70
|
+
export function editorUrl(publicationUrl: string): string {
|
|
71
|
+
const base = publicationUrl
|
|
72
|
+
.trim()
|
|
73
|
+
.replace(/\/$/, '')
|
|
74
|
+
.replace(/\/publish$/, '');
|
|
75
|
+
return `${base}/publish/post/new`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves and validates the run's publication URL (R1, R5): `explicit` — a transport
|
|
80
|
+
* `options.publicationUrl` or the CLI's `--publication` — wins over `SUBSTACK_PUBLICATION_URL`,
|
|
81
|
+
* and a blank explicit value stays blank rather than falling through to the environment. Returns
|
|
82
|
+
* the failure message naming both sources when the tier is unset or is not an `https:` URL, so a
|
|
83
|
+
* publish run and a profile CLI refuse a bad URL identically, before any browser opens.
|
|
84
|
+
*/
|
|
85
|
+
export function resolvePublicationUrl(explicit?: string): { url: string } | { error: string } {
|
|
86
|
+
const trimmed = (explicit ?? process.env.SUBSTACK_PUBLICATION_URL)?.trim() ?? '';
|
|
87
|
+
if (trimmed.length === 0) {
|
|
88
|
+
return { error: `substack publication URL is required: set ${PUBLICATION_URL_SOURCES}` };
|
|
89
|
+
}
|
|
90
|
+
let parsed: URL;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(trimmed);
|
|
93
|
+
} catch {
|
|
94
|
+
return { error: `substack publication URL is not a URL: '${trimmed}' — set ${PUBLICATION_URL_SOURCES}` };
|
|
95
|
+
}
|
|
96
|
+
if (parsed.protocol !== 'https:') {
|
|
97
|
+
return { error: `substack publication URL must be https:, got '${trimmed}' — set ${PUBLICATION_URL_SOURCES}` };
|
|
98
|
+
}
|
|
99
|
+
// Nested editor paths are rejected (residual 0197): `…/publish/posts` used to pass validation
|
|
100
|
+
// and then be mangled by `editorUrl` (`…/publish/posts/publish/post/new`), so a misconfigured
|
|
101
|
+
// run reported an expired session instead of a configuration error. A trailing `/publish` is
|
|
102
|
+
// accepted and stripped by `editorUrl` — that is the documented shape.
|
|
103
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/, '');
|
|
104
|
+
if (trimmedPath.length > 0 && trimmedPath !== '/publish') {
|
|
105
|
+
return {
|
|
106
|
+
error: `substack publication URL must be the publication root, optionally with a trailing /publish (e.g. https://name.substack.com), got '${trimmed}' — set ${PUBLICATION_URL_SOURCES}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { url: trimmed };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Substack-owned readiness predicate: the editor is reachable (no login redirect) and the title
|
|
114
|
+
* field is attached. Shared by the publish preflight and the profile-session CLI (R1, R5).
|
|
115
|
+
*/
|
|
116
|
+
export const isAuthenticated: IsAuthenticatedPredicate = async (page) =>
|
|
117
|
+
!LOGIN_URL_RE.test(page.url()) && (await trySelectors(page, SELECTORS.title, { visible: false })).found;
|
|
118
|
+
|
|
119
|
+
/** Site-owned config for the shared session check and operator CLI (0183). */
|
|
120
|
+
export function channelConfig(publicationUrl: string): ChannelBrowserConfig {
|
|
121
|
+
return {
|
|
122
|
+
channel: CHANNEL,
|
|
123
|
+
homeUrl: editorUrl(publicationUrl),
|
|
124
|
+
isAuthenticated,
|
|
125
|
+
loginTimeoutMs: LOGIN_TIMEOUT_MS,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
16
128
|
|
|
17
129
|
function fail(error: string): Result {
|
|
18
130
|
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
19
131
|
}
|
|
20
132
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
return `${CHANNEL}-draft-${title.length}`;
|
|
133
|
+
function errorMessage(error: unknown): string {
|
|
134
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
135
|
}
|
|
25
136
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
target: TARGET,
|
|
30
|
-
id: draftId(content),
|
|
31
|
-
message: `${TARGET} draft prepared (unpublished)`,
|
|
32
|
-
metadata: { driver, publishStatus: 'draft', profileDir },
|
|
33
|
-
});
|
|
137
|
+
/** Session failures whose one-liner must name the recovery command (R5). */
|
|
138
|
+
function needsLogin(error: unknown): boolean {
|
|
139
|
+
return error instanceof LoginTimeoutError || error instanceof BrowserProfileMissingError;
|
|
34
140
|
}
|
|
35
141
|
|
|
36
142
|
export class SubstackBrowserTransport implements PublishTransport {
|
|
37
143
|
public readonly kind = 'browser';
|
|
38
144
|
public readonly driver: BrowserDriver;
|
|
39
|
-
private readonly
|
|
40
|
-
private readonly
|
|
145
|
+
private readonly launcher?: BrowserLauncher;
|
|
146
|
+
private readonly stepTimeoutMs: number;
|
|
41
147
|
|
|
42
|
-
constructor(driver: BrowserDriver = '
|
|
148
|
+
constructor(driver: BrowserDriver = 'script', deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number } = {}) {
|
|
43
149
|
this.driver = driver;
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
150
|
+
this.launcher = deps.launcher;
|
|
151
|
+
this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
|
|
46
152
|
}
|
|
47
153
|
|
|
48
154
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
155
|
+
if (this.driver !== 'script') {
|
|
156
|
+
return fail(`unsupported driver '${this.driver}' for ${TARGET}`);
|
|
157
|
+
}
|
|
158
|
+
const requested = payload.content.metadata?.publishStatus;
|
|
159
|
+
if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
|
|
160
|
+
return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
|
|
161
|
+
}
|
|
162
|
+
// Pre-validation, before any browser opens: the editor URL is publication-scoped, so an
|
|
163
|
+
// unset or non-https publication URL has no safe default to fall back to (R1).
|
|
164
|
+
const requestedUrl =
|
|
165
|
+
typeof payload.options?.publicationUrl === 'string' ? payload.options.publicationUrl : undefined;
|
|
166
|
+
const publication = resolvePublicationUrl(requestedUrl);
|
|
167
|
+
if ('error' in publication) return fail(publication.error);
|
|
168
|
+
|
|
169
|
+
const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
|
|
170
|
+
const profileDir = resolveChannelProfileDir(CHANNEL, override);
|
|
171
|
+
|
|
172
|
+
// ADR-025 preflight: an unauthenticated or absent profile must never produce a headed
|
|
173
|
+
// visit, so the read-only headless check runs first and a refusal stays a fast refusal.
|
|
174
|
+
const session = await checkChannelSession(channelConfig(publication.url), {
|
|
175
|
+
profileDir,
|
|
176
|
+
launcher: this.launcher,
|
|
177
|
+
headedFallback: false,
|
|
178
|
+
timeoutMs: this.stepTimeoutMs,
|
|
179
|
+
});
|
|
180
|
+
if (session.status !== 'verified') {
|
|
181
|
+
return fail(`${CHANNEL} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const publishStatus = requested === 'public' ? 'public' : 'draft';
|
|
185
|
+
try {
|
|
186
|
+
return await withProfilePage(
|
|
187
|
+
{
|
|
188
|
+
profileDir,
|
|
189
|
+
url: editorUrl(publication.url),
|
|
190
|
+
isAuthenticated,
|
|
191
|
+
// Headed by default (Substack bot-walls headless loads); the headless preflight
|
|
192
|
+
// above is the unattended pass, `options.headless === true` the opt-in.
|
|
193
|
+
headless: payload.options?.headless === true,
|
|
194
|
+
launcher: this.launcher,
|
|
195
|
+
// The session is already preflight-verified, so the post-goto confirmation is
|
|
196
|
+
// a bounded step like every other one, not the 180s interactive login wait.
|
|
197
|
+
timeoutMs: this.stepTimeoutMs,
|
|
198
|
+
},
|
|
199
|
+
async ({ page }) => this.runEditorFlow(page, payload.content, publishStatus, profileDir),
|
|
57
200
|
);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const hint = needsLogin(error) ? ` — run: ${LOGIN_INSTRUCTION}` : '';
|
|
203
|
+
return fail(`${errorMessage(error)}${hint}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The site-owned editor flow: title → optional subtitle → ProseMirror HTML paste → the draft
|
|
209
|
+
* signal, then — for a live run only — `Continue` → `Send to everyone now` → the live signal.
|
|
210
|
+
* Substack autosaves the draft, so the draft needs no button click and both outcomes come
|
|
211
|
+
* from an `expectOutcome` probe (E91-D6): a wait without an observed signal is never a success.
|
|
212
|
+
*/
|
|
213
|
+
private async runEditorFlow(
|
|
214
|
+
page: Page,
|
|
215
|
+
content: Content,
|
|
216
|
+
publishStatus: 'draft' | 'public',
|
|
217
|
+
profileDir: string,
|
|
218
|
+
): Promise<Result> {
|
|
219
|
+
const metadata = { driver: this.driver, publishStatus, profileDir };
|
|
220
|
+
|
|
221
|
+
const titleSelector = await resolveSelector(page, SELECTORS.title, {
|
|
222
|
+
timeoutMs: this.stepTimeoutMs,
|
|
223
|
+
label: 'title input',
|
|
224
|
+
});
|
|
225
|
+
await page
|
|
226
|
+
.locator(titleSelector)
|
|
227
|
+
.first()
|
|
228
|
+
.fill(content.title ?? '');
|
|
229
|
+
|
|
230
|
+
const subtitle = content.metadata?.subtitle;
|
|
231
|
+
if (typeof subtitle === 'string' && subtitle.length > 0) {
|
|
232
|
+
const subtitleSelector = await resolveSelector(page, SELECTORS.subtitle, {
|
|
233
|
+
timeoutMs: this.stepTimeoutMs,
|
|
234
|
+
label: 'subtitle input',
|
|
235
|
+
});
|
|
236
|
+
await page.locator(subtitleSelector).first().fill(subtitle);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const bodySelector = await resolveSelector(page, SELECTORS.body, {
|
|
240
|
+
timeoutMs: this.stepTimeoutMs,
|
|
241
|
+
label: 'body editor',
|
|
242
|
+
});
|
|
243
|
+
// Substack's editor is ProseMirror rich text: the body goes in as rendered HTML through the
|
|
244
|
+
// shared paste, never as plain markdown text (E91-D4).
|
|
245
|
+
await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
|
|
246
|
+
|
|
247
|
+
const draft = await expectOutcome(() => probeDraft(page), {
|
|
248
|
+
timeoutMs: this.stepTimeoutMs,
|
|
249
|
+
label: `substack draft signal in '${bodySelector}'`,
|
|
250
|
+
});
|
|
251
|
+
if (!draft.ok) return fail(draft.error);
|
|
252
|
+
|
|
253
|
+
// The probe only ever returns a `/publish/post/<id>` URL, so this capture always resolves.
|
|
254
|
+
const id = DRAFT_URL_RE.exec(draft.evidence)?.[1];
|
|
255
|
+
if (publishStatus === 'draft') {
|
|
256
|
+
return ResultSchema.parse({
|
|
257
|
+
ok: true,
|
|
258
|
+
target: TARGET,
|
|
259
|
+
...(id === undefined ? {} : { id }),
|
|
260
|
+
url: draft.evidence,
|
|
261
|
+
message: `${TARGET} draft saved (unpublished) in profile ${profileDir}`,
|
|
262
|
+
metadata,
|
|
263
|
+
});
|
|
58
264
|
}
|
|
59
|
-
|
|
265
|
+
|
|
266
|
+
const continueSelector = await resolveSelector(page, SELECTORS.continue, {
|
|
267
|
+
timeoutMs: this.stepTimeoutMs,
|
|
268
|
+
label: 'continue control',
|
|
269
|
+
});
|
|
270
|
+
await page.locator(continueSelector).first().click();
|
|
271
|
+
const sendSelector = await resolveSelector(page, SELECTORS.send, {
|
|
272
|
+
timeoutMs: this.stepTimeoutMs,
|
|
273
|
+
label: 'send control',
|
|
274
|
+
});
|
|
275
|
+
await page.locator(sendSelector).first().click();
|
|
276
|
+
const live = await expectOutcome(() => probeLive(page), {
|
|
277
|
+
timeoutMs: this.stepTimeoutMs,
|
|
278
|
+
label: `substack publish '${sendSelector}'`,
|
|
279
|
+
});
|
|
280
|
+
if (!live.ok) return fail(live.error);
|
|
281
|
+
|
|
282
|
+
return ResultSchema.parse({
|
|
283
|
+
ok: true,
|
|
284
|
+
target: TARGET,
|
|
285
|
+
...(id === undefined ? {} : { id }),
|
|
286
|
+
url: live.evidence,
|
|
287
|
+
message: `${TARGET} published live in profile ${profileDir}`,
|
|
288
|
+
metadata,
|
|
289
|
+
});
|
|
60
290
|
}
|
|
61
291
|
}
|
|
62
292
|
|
|
63
|
-
|
|
293
|
+
/** Draft evidence is URL-only by design: Substack's autosave rewrites `/publish/post/new`. */
|
|
294
|
+
async function probeDraft(page: Page): Promise<string | null> {
|
|
295
|
+
const url = page.url();
|
|
296
|
+
return DRAFT_URL_RE.test(url) ? url : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Live evidence is URL-only too: `/p/<slug>` (the published post) or the publish queue. */
|
|
300
|
+
async function probeLive(page: Page): Promise<string | null> {
|
|
301
|
+
const url = page.url();
|
|
302
|
+
return LIVE_URL_RE.test(url) ? url : null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function getPublishTransport(driver: BrowserDriver = 'script'): PublishTransport {
|
|
64
306
|
if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
|
|
65
|
-
return new FakeBrowserTransport(
|
|
307
|
+
return new FakeBrowserTransport('script');
|
|
66
308
|
}
|
|
67
309
|
return new SubstackBrowserTransport(driver);
|
|
68
310
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substack profile-session capability (R5, AC3).
|
|
3
|
+
*
|
|
4
|
+
* `login` opens the publication's editor in a headed browser and resolves only once Substack's
|
|
5
|
+
* readiness predicate passes; `check` reopens the profile headlessly and reports `verified` or
|
|
6
|
+
* `refused`. Arg parsing, profile precedence (`--profile` > `SUBSTACK_PROFILE_DIR` >
|
|
7
|
+
* `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/substack`) and the exit codes
|
|
8
|
+
* (0 verified, 1 refused, 2 usage error) all live in the shared `runProfileCli` (0183), so the
|
|
9
|
+
* publish preflight and this CLI apply the same gate.
|
|
10
|
+
*
|
|
11
|
+
* The publication is publication-scoped, so it comes from `--publication <url>` or
|
|
12
|
+
* `SUBSTACK_PUBLICATION_URL`; `--publication` is stripped before `runProfileCli` parses the rest
|
|
13
|
+
* (the shared CLI rejects a flag it does not know), and a missing or non-https value is a usage
|
|
14
|
+
* error that opens no browser.
|
|
15
|
+
*
|
|
16
|
+
* Operator dogfood (plan step 6, handed to 0193): run `profile-check.ts login`, then `check`, on
|
|
17
|
+
* the same profile, and re-measure the editor URL, the selectors and the draft signal.
|
|
18
|
+
*/
|
|
19
|
+
import { type ChannelBrowserConfig, type ProfileCliDeps, runProfileCli } from '@gobing-ai/publish-harness/browser';
|
|
20
|
+
import { echo } from '@gobing-ai/ts-utils';
|
|
21
|
+
import { channelConfig, resolvePublicationUrl } from './index';
|
|
22
|
+
|
|
23
|
+
const PUBLICATION_FLAG = '--publication';
|
|
24
|
+
const USAGE = 'usage: substack profile-check [login|check] [--publication <url>] [--profile <dir>] [--timeout <ms>]';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Splits `--publication <url>` / `--publication=<url>` out of the operator argv; `rest` keeps the
|
|
28
|
+
* `login|check` positional and the `--profile` / `--timeout` flags `runProfileCli` owns. A flag
|
|
29
|
+
* with no value stays an empty string, so it is refused as a blank publication URL instead of
|
|
30
|
+
* silently falling through to the environment.
|
|
31
|
+
*/
|
|
32
|
+
export function splitPublicationFlag(argv: readonly string[]): { publicationUrl?: string; rest: string[] } {
|
|
33
|
+
const rest: string[] = [];
|
|
34
|
+
let publicationUrl: string | undefined;
|
|
35
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
+
const arg = argv[index] ?? '';
|
|
37
|
+
if (arg === PUBLICATION_FLAG) {
|
|
38
|
+
publicationUrl = argv[index + 1] ?? '';
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg.startsWith(`${PUBLICATION_FLAG}=`)) {
|
|
43
|
+
publicationUrl = arg.slice(PUBLICATION_FLAG.length + 1);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
rest.push(arg);
|
|
47
|
+
}
|
|
48
|
+
return { publicationUrl, rest };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `login|check` for one publication: the shared CLI over `channelConfig(publicationUrl)`.
|
|
53
|
+
* Exit codes: 0 verified (or logged in), 1 refused, 2 usage error — the publication URL is
|
|
54
|
+
* resolved (and refused) before anything launches.
|
|
55
|
+
*/
|
|
56
|
+
export async function main(argv: readonly string[] = Bun.argv.slice(2), deps: ProfileCliDeps = {}): Promise<number> {
|
|
57
|
+
const stdout =
|
|
58
|
+
deps.stdout ??
|
|
59
|
+
((line: string) => {
|
|
60
|
+
echo(line);
|
|
61
|
+
});
|
|
62
|
+
const { publicationUrl, rest } = splitPublicationFlag(argv);
|
|
63
|
+
const publication = resolvePublicationUrl(publicationUrl);
|
|
64
|
+
if ('error' in publication) {
|
|
65
|
+
stdout(`substack profile-check: ${publication.error} — ${USAGE}`);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
const config: ChannelBrowserConfig = channelConfig(publication.url);
|
|
69
|
+
return runProfileCli(config, [...rest], { ...deps, stdout });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (import.meta.main) {
|
|
73
|
+
process.exit(await main());
|
|
74
|
+
}
|
|
@@ -14665,7 +14665,7 @@ var init_zod = __esm(() => {
|
|
|
14665
14665
|
init_external();
|
|
14666
14666
|
});
|
|
14667
14667
|
|
|
14668
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14668
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
|
|
14669
14669
|
var ErrorCode;
|
|
14670
14670
|
var init_errors3 = __esm(() => {
|
|
14671
14671
|
ErrorCode = {
|
|
@@ -14676,7 +14676,7 @@ var init_errors3 = __esm(() => {
|
|
|
14676
14676
|
};
|
|
14677
14677
|
});
|
|
14678
14678
|
|
|
14679
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14679
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
|
|
14680
14680
|
var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
|
|
14681
14681
|
var init_api_response = __esm(() => {
|
|
14682
14682
|
init_errors3();
|
|
@@ -14699,13 +14699,13 @@ var init_api_response = __esm(() => {
|
|
|
14699
14699
|
CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
|
|
14700
14700
|
});
|
|
14701
14701
|
|
|
14702
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14702
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
|
|
14703
14703
|
var init_cursor = () => {};
|
|
14704
14704
|
|
|
14705
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14705
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
|
|
14706
14706
|
var init_event_severity = () => {};
|
|
14707
14707
|
|
|
14708
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14708
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
|
|
14709
14709
|
function processStream(name) {
|
|
14710
14710
|
const proc = globalThis.process;
|
|
14711
14711
|
const stream = proc?.[name];
|
|
@@ -14723,7 +14723,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
14723
14723
|
}
|
|
14724
14724
|
var defaultStderrTarget;
|
|
14725
14725
|
|
|
14726
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14726
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
|
|
14727
14727
|
var init_dist = __esm(() => {
|
|
14728
14728
|
init_api_response();
|
|
14729
14729
|
init_cursor();
|
|
@@ -21721,7 +21721,7 @@ var init_dist2 = __esm(() => {
|
|
|
21721
21721
|
$visitAsync = visit.visitAsync;
|
|
21722
21722
|
});
|
|
21723
21723
|
|
|
21724
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21724
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
|
|
21725
21725
|
var configSchema;
|
|
21726
21726
|
var init_config = __esm(() => {
|
|
21727
21727
|
init_dist2();
|
|
@@ -21745,7 +21745,7 @@ var init_config = __esm(() => {
|
|
|
21745
21745
|
});
|
|
21746
21746
|
});
|
|
21747
21747
|
|
|
21748
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21748
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
|
|
21749
21749
|
import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
21750
21750
|
import { dirname, resolve as resolvePath } from "path";
|
|
21751
21751
|
function createNodeFileSystem(root, paths) {
|
|
@@ -22041,7 +22041,7 @@ var PluginManifestSchema = exports_external.looseObject({
|
|
|
22041
22041
|
description: exports_external.string().optional(),
|
|
22042
22042
|
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
22043
22043
|
});
|
|
22044
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
22044
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
22045
22045
|
init_file_system_node();
|
|
22046
22046
|
init_config();
|
|
22047
22047
|
|