@gobing-ai/knowledge-kit 0.0.30 → 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.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +10 -10
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +9 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +12 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +12 -12
  7. package/plugins/generations/dailynews-gen/dist/index.js +10 -10
  8. package/plugins/generations/dailynews-gen/src/script-builder.ts +4 -2
  9. package/plugins/generations/episode-plan-gen/dist/index.js +23 -13
  10. package/plugins/generations/episode-plan-gen/src/index.ts +23 -2
  11. package/plugins/generations/gemini-tts/dist/index.js +9 -9
  12. package/plugins/generations/image-gen/dist/index.js +9 -9
  13. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  14. package/plugins/generations/voice-gen/dist/index.js +9 -9
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  17. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  18. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  19. package/plugins/ingestions/web-search/dist/index.js +10 -10
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/kk-workflow-stages.ts +173 -15
  22. package/plugins/kk/skills/content-judge/SKILL.md +16 -4
  23. package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
  24. package/plugins/kk/skills/content-judge/references/workflow-integration.md +2 -1
  25. package/plugins/kk/skills/content-safety/SKILL.md +24 -13
  26. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +65 -8
  27. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  28. package/plugins/publishings/infoq-pub/dist/index.js +8602 -211
  29. package/plugins/publishings/infoq-pub/package.json +3 -1
  30. package/plugins/publishings/infoq-pub/plugin.json +1 -1
  31. package/plugins/publishings/infoq-pub/src/index.ts +246 -29
  32. package/plugins/publishings/infoq-pub/src/profile-check.ts +19 -0
  33. package/plugins/publishings/juejin-pub/dist/index.js +8665 -211
  34. package/plugins/publishings/juejin-pub/package.json +3 -1
  35. package/plugins/publishings/juejin-pub/plugin.json +1 -1
  36. package/plugins/publishings/juejin-pub/src/index.ts +305 -29
  37. package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
  38. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  39. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  40. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  41. package/plugins/publishings/substack-pub/dist/index.js +8610 -211
  42. package/plugins/publishings/substack-pub/package.json +3 -1
  43. package/plugins/publishings/substack-pub/plugin.json +1 -1
  44. package/plugins/publishings/substack-pub/src/index.ts +271 -29
  45. package/plugins/publishings/substack-pub/src/profile-check.ts +74 -0
  46. package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
  47. package/plugins/publishings/wechatmp-pub/dist/index.js +1227 -348
  48. package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
  49. package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
  50. package/plugins/publishings/wechatmp-pub/src/index.ts +186 -516
  51. package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
  52. package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
  53. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -5
  54. package/plugins/publishings/x-pub/dist/index.js +8626 -221
  55. package/plugins/publishings/x-pub/package.json +3 -1
  56. package/plugins/publishings/x-pub/plugin.json +1 -1
  57. package/plugins/publishings/x-pub/src/index.ts +258 -20
  58. package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
  59. package/plugins/publishings/xhs-pub/dist/index.js +843 -346
  60. package/plugins/publishings/xhs-pub/plugin.json +1 -1
  61. package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
  62. package/plugins/publishings/xhs-pub/src/index.ts +101 -593
  63. package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
  64. package/plugins/publishings/xhs-pub/src/page.ts +267 -0
  65. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -3
  66. 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/agent transport"
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
- resolveBrowserProfileDir,
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
- const CHANNEL = 'substack';
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 draftId(content: Content): string {
22
- const title = content.title ?? 'untitled';
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
- function draftResult(content: Content, driver: BrowserDriver, profileDir: string): Result {
27
- return ResultSchema.parse({
28
- ok: true,
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 loginTimeoutMs: number;
40
- private readonly sessionReady: boolean;
145
+ private readonly launcher?: BrowserLauncher;
146
+ private readonly stepTimeoutMs: number;
41
147
 
42
- constructor(driver: BrowserDriver = 'agent', opts?: { loginTimeoutMs?: number; sessionReady?: boolean }) {
148
+ constructor(driver: BrowserDriver = 'script', deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number } = {}) {
43
149
  this.driver = driver;
44
- this.loginTimeoutMs = opts?.loginTimeoutMs ?? 120_000;
45
- this.sessionReady = opts?.sessionReady ?? false;
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
- const override =
50
- typeof payload.options?.profileDir === 'string'
51
- ? payload.options.profileDir
52
- : (process.env.SUBSTACK_PROFILE_DIR ?? process.env.KK_BROWSER_PROFILE_DIR);
53
- const profileDir = resolveBrowserProfileDir(CHANNEL, override);
54
- if (!this.sessionReady) {
55
- return fail(
56
- `login timeout after ${this.loginTimeoutMs}ms: no authenticated ${CHANNEL} session in ${profileDir}`,
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
- return draftResult(payload.content, this.driver, profileDir);
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
- export function getPublishTransport(driver: BrowserDriver = 'agent'): PublishTransport {
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(driver);
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.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
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.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
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.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
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.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
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.11/node_modules/@gobing-ai/ts-utils/dist/output.js
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.11/node_modules/@gobing-ai/ts-utils/dist/index.js
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.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
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.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
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.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
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