@supportpages.io/wtfm 0.0.0-stage → 0.3.0

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 (275) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +23 -0
  3. package/README.md +392 -2
  4. package/dist/actions.d.ts +98 -0
  5. package/dist/actions.js +86 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-settings.d.ts +22 -0
  8. package/dist/agent-settings.js +35 -0
  9. package/dist/agent-settings.js.map +1 -0
  10. package/dist/api.d.ts +14 -0
  11. package/dist/api.js +159 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/article-link.d.ts +16 -0
  14. package/dist/article-link.js +25 -0
  15. package/dist/article-link.js.map +1 -0
  16. package/dist/artifacts.d.ts +45 -0
  17. package/dist/artifacts.js +144 -0
  18. package/dist/artifacts.js.map +1 -0
  19. package/dist/brand.d.ts +4 -0
  20. package/dist/brand.js +9 -0
  21. package/dist/brand.js.map +1 -0
  22. package/dist/bridge.d.ts +2964 -0
  23. package/dist/bridge.js +1036 -0
  24. package/dist/bridge.js.map +1 -0
  25. package/dist/capacity.d.ts +5 -0
  26. package/dist/capacity.js +11 -0
  27. package/dist/capacity.js.map +1 -0
  28. package/dist/credentials.d.ts +13 -0
  29. package/dist/credentials.js +88 -0
  30. package/dist/credentials.js.map +1 -0
  31. package/dist/development-tls.d.ts +8 -0
  32. package/dist/development-tls.js +38 -0
  33. package/dist/development-tls.js.map +1 -0
  34. package/dist/errors.d.ts +15 -0
  35. package/dist/errors.js +18 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/export.d.ts +11 -0
  38. package/dist/export.js +91 -0
  39. package/dist/export.js.map +1 -0
  40. package/dist/hosted-operations.d.ts +108 -0
  41. package/dist/hosted-operations.js +120 -0
  42. package/dist/hosted-operations.js.map +1 -0
  43. package/dist/hosting-benefits.d.ts +66 -0
  44. package/dist/hosting-benefits.js +67 -0
  45. package/dist/hosting-benefits.js.map +1 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +49 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/local-inventory.d.ts +22 -0
  50. package/dist/local-inventory.js +59 -0
  51. package/dist/local-inventory.js.map +1 -0
  52. package/dist/local-setup.d.ts +208 -0
  53. package/dist/local-setup.js +140 -0
  54. package/dist/local-setup.js.map +1 -0
  55. package/dist/pairing.d.ts +71 -0
  56. package/dist/pairing.js +235 -0
  57. package/dist/pairing.js.map +1 -0
  58. package/dist/preferences.d.ts +18 -0
  59. package/dist/preferences.js +44 -0
  60. package/dist/preferences.js.map +1 -0
  61. package/dist/progress.d.ts +205 -0
  62. package/dist/progress.js +224 -0
  63. package/dist/progress.js.map +1 -0
  64. package/dist/reminders.d.ts +31 -0
  65. package/dist/reminders.js +73 -0
  66. package/dist/reminders.js.map +1 -0
  67. package/dist/replace-connection.d.ts +6 -0
  68. package/dist/replace-connection.js +77 -0
  69. package/dist/replace-connection.js.map +1 -0
  70. package/dist/repository-actions.d.ts +18 -0
  71. package/dist/repository-actions.js +9 -0
  72. package/dist/repository-actions.js.map +1 -0
  73. package/dist/repository-benefits.d.ts +96 -0
  74. package/dist/repository-benefits.js +64 -0
  75. package/dist/repository-benefits.js.map +1 -0
  76. package/dist/run-update.d.ts +17 -0
  77. package/dist/run-update.js +27 -0
  78. package/dist/run-update.js.map +1 -0
  79. package/dist/runs.d.ts +541 -0
  80. package/dist/runs.js +146 -0
  81. package/dist/runs.js.map +1 -0
  82. package/dist/runtime.d.ts +12 -0
  83. package/dist/runtime.js +58 -0
  84. package/dist/runtime.js.map +1 -0
  85. package/dist/schema.d.ts +277 -0
  86. package/dist/schema.js +66 -0
  87. package/dist/schema.js.map +1 -0
  88. package/dist/server.d.ts +4 -0
  89. package/dist/server.js +298 -0
  90. package/dist/server.js.map +1 -0
  91. package/dist/session.d.ts +1714 -0
  92. package/dist/session.js +619 -0
  93. package/dist/session.js.map +1 -0
  94. package/dist/settings.d.ts +9 -0
  95. package/dist/settings.js +21 -0
  96. package/dist/settings.js.map +1 -0
  97. package/dist/sync.d.ts +495 -0
  98. package/dist/sync.js +191 -0
  99. package/dist/sync.js.map +1 -0
  100. package/dist/telemetry-scrub.d.ts +13 -0
  101. package/dist/telemetry-scrub.js +51 -0
  102. package/dist/telemetry-scrub.js.map +1 -0
  103. package/dist/telemetry.d.ts +73 -0
  104. package/dist/telemetry.js +173 -0
  105. package/dist/telemetry.js.map +1 -0
  106. package/dist/walkthroughs.d.ts +224 -0
  107. package/dist/walkthroughs.js +109 -0
  108. package/dist/walkthroughs.js.map +1 -0
  109. package/dist/workspace.d.ts +14 -0
  110. package/dist/workspace.js +127 -0
  111. package/dist/workspace.js.map +1 -0
  112. package/dist/writer-agent.d.ts +21 -0
  113. package/dist/writer-agent.js +27 -0
  114. package/dist/writer-agent.js.map +1 -0
  115. package/dist/writer-entry.d.ts +171 -0
  116. package/dist/writer-entry.js +233 -0
  117. package/dist/writer-entry.js.map +1 -0
  118. package/dist/writing-style.d.ts +7 -0
  119. package/dist/writing-style.js +52 -0
  120. package/dist/writing-style.js.map +1 -0
  121. package/engine/SYNC.json +4 -0
  122. package/engine/VERSION +1 -0
  123. package/engine/detect-project/README.md +141 -0
  124. package/engine/detect-project/SKILL.md +1421 -0
  125. package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
  126. package/engine/detect-project/assets/game/game-frame.css +132 -0
  127. package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
  128. package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
  129. package/engine/detect-project/assets/macosui/fonts.css +15 -0
  130. package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
  131. package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
  132. package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
  133. package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
  134. package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
  135. package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
  136. package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
  137. package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
  138. package/engine/detect-project/assets/mobileui/fonts.css +16 -0
  139. package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
  140. package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
  141. package/engine/detect-project/assets/mobileui/icons.css +31 -0
  142. package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
  143. package/engine/detect-project/assets/mobileui/platforms.json +46 -0
  144. package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
  145. package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
  146. package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
  147. package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
  148. package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
  149. package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
  150. package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
  151. package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
  152. package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
  153. package/engine/detect-project/assets/webtui/themes.json +37 -0
  154. package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
  155. package/engine/detect-project/assets/win32ui/7css.css +2 -0
  156. package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
  157. package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
  158. package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
  159. package/engine/detect-project/package-lock.json +12 -0
  160. package/engine/detect-project/package.json +10 -0
  161. package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
  162. package/engine/detect-project/scripts/check_css_health.js +412 -0
  163. package/engine/detect-project/scripts/check_project_map.js +150 -0
  164. package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
  165. package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
  166. package/engine/detect-project/scripts/classify_app_type.sh +246 -0
  167. package/engine/detect-project/scripts/classify_surface.sh +95 -0
  168. package/engine/detect-project/scripts/classify_workspace.js +39 -0
  169. package/engine/detect-project/scripts/compile_css.sh +447 -0
  170. package/engine/detect-project/scripts/detect_static.js +645 -0
  171. package/engine/detect-project/scripts/detect_structure.js +187 -0
  172. package/engine/detect-project/scripts/include_census.js +451 -0
  173. package/engine/detect-project/scripts/json_get.js +142 -0
  174. package/engine/detect-project/scripts/merge_json.js +52 -0
  175. package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
  176. package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
  177. package/engine/detect-project/scripts/run_css_build.sh +40 -0
  178. package/engine/detect-project/scripts/sanitize_css.js +83 -0
  179. package/engine/detect-project/scripts/test_classify_surface.js +108 -0
  180. package/engine/detect-project/scripts/test_node_helpers.js +142 -0
  181. package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
  182. package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
  183. package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
  184. package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
  185. package/engine/detect-project/scripts/theme_overrides.js +169 -0
  186. package/engine/detect-project/scripts/write_branding.js +129 -0
  187. package/engine/generate-illustrated-article/SKILL.md +461 -0
  188. package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
  189. package/engine/generate-illustrated-article/contracts/game.md +38 -0
  190. package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
  191. package/engine/generate-illustrated-article/contracts/macos.md +79 -0
  192. package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
  193. package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
  194. package/engine/generate-illustrated-article/contracts/win32.md +43 -0
  195. package/engine/generate-illustrated-article/package-lock.json +366 -0
  196. package/engine/generate-illustrated-article/package.json +15 -0
  197. package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
  198. package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
  199. package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
  200. package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
  201. package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
  202. package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
  203. package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
  204. package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
  205. package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
  206. package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
  207. package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
  208. package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
  209. package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
  210. package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
  211. package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
  212. package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
  213. package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
  214. package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
  215. package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
  216. package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
  217. package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
  218. package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
  219. package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
  220. package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
  221. package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
  222. package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
  223. package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
  224. package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
  225. package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
  226. package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
  227. package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
  228. package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
  229. package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
  230. package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
  231. package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
  232. package/install.sh +14 -0
  233. package/package.json +57 -4
  234. package/scripts/auth.mjs +33 -0
  235. package/scripts/auto-update.mjs +7 -0
  236. package/scripts/build-plugin.mjs +58 -0
  237. package/scripts/build-release.mjs +94 -0
  238. package/scripts/check-release.mjs +40 -0
  239. package/scripts/check-runtime.mjs +10 -0
  240. package/scripts/cli.mjs +8 -0
  241. package/scripts/install.mjs +64 -0
  242. package/scripts/lib/agent-runner.mjs +181 -0
  243. package/scripts/lib/agent-settings.mjs +215 -0
  244. package/scripts/lib/article-skills.mjs +82 -0
  245. package/scripts/lib/auto-update.mjs +50 -0
  246. package/scripts/lib/brand.mjs +9 -0
  247. package/scripts/lib/browser.mjs +12 -0
  248. package/scripts/lib/claude-connection.mjs +33 -0
  249. package/scripts/lib/claude-permissions.mjs +47 -0
  250. package/scripts/lib/claude-plugin.mjs +15 -0
  251. package/scripts/lib/claude-writer.mjs +10 -0
  252. package/scripts/lib/cli-main.mjs +85 -0
  253. package/scripts/lib/cli.mjs +888 -0
  254. package/scripts/lib/codex-config.mjs +51 -0
  255. package/scripts/lib/codex-integration.mjs +61 -0
  256. package/scripts/lib/codex-skill.mjs +34 -0
  257. package/scripts/lib/harness-models.mjs +109 -0
  258. package/scripts/lib/install.mjs +269 -0
  259. package/scripts/lib/managed-writer.mjs +38 -0
  260. package/scripts/lib/planning.mjs +263 -0
  261. package/scripts/lib/prepare-update.mjs +85 -0
  262. package/scripts/lib/refresh-writers.mjs +12 -0
  263. package/scripts/lib/remove.mjs +167 -0
  264. package/scripts/lib/renderer.mjs +34 -0
  265. package/scripts/lib/terminal.mjs +252 -0
  266. package/scripts/lib/uninit.mjs +72 -0
  267. package/scripts/lib/update.mjs +55 -0
  268. package/scripts/lib/writer-recovery.mjs +35 -0
  269. package/scripts/lib/yolo.mjs +362 -0
  270. package/scripts/plugin-session.mjs +65 -0
  271. package/scripts/prepare-update.mjs +12 -0
  272. package/scripts/publish-release.mjs +92 -0
  273. package/scripts/skills/supportpages/SKILL.md +120 -0
  274. package/scripts/smoke-release.mjs +101 -0
  275. package/server.json +27 -0
@@ -0,0 +1,619 @@
1
+ import os from 'node:os';
2
+ import { resolveAction } from './actions.js';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { lstat, rm } from 'node:fs/promises';
6
+ import { Workspace } from './workspace.js';
7
+ import { Bridge, analysisInstruction } from './bridge.js';
8
+ import { ApiClient, connectionStateRoot } from './api.js';
9
+ import { Runs } from './runs.js';
10
+ import { readCredential, readTokenFile, saveTokenFile } from './credentials.js';
11
+ import { Pairing } from './pairing.js';
12
+ import { fail, publicError } from './errors.js';
13
+ import { replaceConnection } from './replace-connection.js';
14
+ import { configureDevelopmentTLS } from './development-tls.js';
15
+ import { openPreview } from './progress.js';
16
+ import { CLI_NAME } from './brand.js';
17
+ /** One credential per API origin: signing in covers every help centre the account can access. */
18
+ export function credentialLocation(origin, configDir) {
19
+ const reference = createHash('sha256').update(origin).digest('hex').slice(0, 12);
20
+ return { reference, filename: path.join(configDir, 'credentials', `${reference}.json`) };
21
+ }
22
+ export const defaultConfigDir = () => path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'supportpages');
23
+ /** Bounded display label for the approval page; never a path. */
24
+ export function deviceLabel(hostname = os.hostname()) {
25
+ return [...hostname.replace(/[\p{Cc}\p{Cf}]/gu, '').trim()].slice(0, 100).join('') || 'This device';
26
+ }
27
+ const LOGIN = '\0login';
28
+ export class Session {
29
+ options;
30
+ roots;
31
+ selected;
32
+ pairings = new Map();
33
+ initializing = new Map();
34
+ hostedConnecting = new Map();
35
+ setupPages = new Map();
36
+ openBrowser = openPreview;
37
+ approval;
38
+ approvalCompleted;
39
+ /** Form elicitation: the client renders the question itself, so the choice never
40
+ * depends on how the model phrases it. Undefined when the client has no dialog. */
41
+ askForm;
42
+ declinedAccount = new Set();
43
+ bridges = new Map();
44
+ closed = false;
45
+ close() { for (const { bridge } of this.bridges.values())
46
+ bridge.close(); this.closed = true; for (const pairing of this.pairings.values())
47
+ pairing.cancel(); }
48
+ constructor(options) {
49
+ this.options = options;
50
+ configureDevelopmentTLS(options.origin, options.dev);
51
+ }
52
+ async workspace(requested) {
53
+ const fixed = this.options.workspace ?? this.options.projectDir;
54
+ const roots = fixed ? undefined : await this.roots?.();
55
+ if (roots && roots.length === 0)
56
+ fail('workspace_required', 'The client supplied no local workspace roots. Configure --workspace explicitly.');
57
+ const candidates = fixed ? [fixed] : roots ?? [this.options.cwd];
58
+ const canonical = await Promise.all(candidates.map(async (root) => (await Workspace.create(root)).root));
59
+ const choice = requested ?? this.selected;
60
+ if (choice) {
61
+ const resolved = (await Workspace.create(choice)).root;
62
+ if (!canonical.includes(resolved))
63
+ fail('invalid_workspace', 'Choose one of the workspace roots supplied by the client.', { roots: canonical });
64
+ this.selected = resolved;
65
+ return Workspace.create(resolved);
66
+ }
67
+ if (canonical.length !== 1)
68
+ fail('workspace_required', 'Multiple workspace roots are available. Call supportpages_init with the intended workspace.', { roots: canonical });
69
+ return Workspace.create(canonical[0]);
70
+ }
71
+ /** Explicit token overrides win; otherwise the device credential for this origin; otherwise an inherited environment token. */
72
+ async credential() {
73
+ const o = this.options;
74
+ if (o.tokenFile)
75
+ return readCredential(o.tokenFile, o.origin);
76
+ const { filename } = credentialLocation(o.origin, o.configDir);
77
+ try {
78
+ await lstat(filename);
79
+ }
80
+ catch (error) {
81
+ if (error.code !== 'ENOENT')
82
+ throw error;
83
+ return o.token ? { token: o.token } : undefined;
84
+ }
85
+ return readCredential(filename, o.origin);
86
+ }
87
+ async account() {
88
+ try {
89
+ return (await this.credential())?.account;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ async bridge(ws, options = {}) {
96
+ ws ??= await this.workspace();
97
+ const o = this.options;
98
+ const token = (await this.credential())?.token;
99
+ const key = ws.root;
100
+ const existing = this.bridges.get(key);
101
+ if (existing && existing.token === token)
102
+ return existing.bridge;
103
+ const bridge = new Bridge(ws, new ApiClient(o.origin, token, (...args) => fetch(...args), o.dev), o.skillsDir, undefined, undefined, o.configDir);
104
+ // Invitation tools only inspect metadata. Do not arm a completion relay or
105
+ // cache this passive instance in place of the normal session-owned bridge.
106
+ if (options.resumeCompletion === false)
107
+ return bridge;
108
+ existing?.bridge.close();
109
+ await bridge.resumeWriterCompletion();
110
+ this.bridges.set(key, { token, bridge });
111
+ return bridge;
112
+ }
113
+ /** Sign this device in. Reuses a working credential; otherwise runs browser approval and saves the delivered account token. */
114
+ async login(input = {}) {
115
+ const o = this.options;
116
+ if (o.tokenFile || o.token)
117
+ fail('invalid_configuration', 'Sign-in manages the saved device credential. Remove explicit token overrides first.');
118
+ let existing;
119
+ try {
120
+ existing = await this.credential();
121
+ }
122
+ catch { /* An unreadable file is replaced by a fresh sign-in. */ }
123
+ if (existing) {
124
+ try {
125
+ const settings = await new ApiClient(o.origin, existing.token, fetch, o.dev).request('GET', '/mcp/settings');
126
+ if (!input.scopes?.length || input.scopes.every(scope => settings?.scopes?.includes(scope)))
127
+ return { status: 'signed_in', api_origin: o.origin, account: settings?.account ?? existing.account, already_signed_in: true };
128
+ if (!settings?.scopes)
129
+ fail('server_update_required', 'Update the server before requesting new permissions.');
130
+ input.scopes = [...new Set([...input.scopes, ...settings.scopes.filter(scope => ['publish', 'manage', 'generate'].includes(scope))])];
131
+ }
132
+ catch (error) {
133
+ if (error.code !== 'invalid_credentials')
134
+ throw error;
135
+ }
136
+ }
137
+ const pairing = new Pairing(o.origin, o.dev, o.pairingRuntime);
138
+ const request = await pairing.start(deviceLabel(), { client: 'SupportPages Writer' });
139
+ if (this.closed) {
140
+ pairing.cancel();
141
+ fail('authorization_cancelled', 'Sign-in cancelled.');
142
+ }
143
+ this.pairings.set(LOGIN, pairing);
144
+ try {
145
+ await input.onApproval?.(request);
146
+ let delivery;
147
+ pairing.run(async (value) => { delivery = value; await this.saveCredential(value); });
148
+ while (!pairing.finished)
149
+ await pairing.wait(1000);
150
+ if (pairing.result().status !== 'ready')
151
+ fail('authorization_cancelled', 'Sign-in cancelled.');
152
+ return { status: 'signed_in', api_origin: o.origin, account: delivery?.account };
153
+ }
154
+ finally {
155
+ this.pairings.delete(LOGIN);
156
+ }
157
+ }
158
+ /** A new request never expands the existing token. Only the explicit browser
159
+ * grant can deliver a replacement credential. Declining keeps the old one. */
160
+ async requestPermissions(input) {
161
+ if (this.options.tokenFile || this.options.token)
162
+ fail('permission_required', 'Update the explicitly configured token in SupportPages settings; browser consent cannot replace a token override.');
163
+ const ws = await this.workspace(input.workspace);
164
+ const decision = await resolveAction(await this.bridge(ws, { resumeCompletion: false }), input.action, input.article_id);
165
+ if (decision.next_step?.code !== 'permission_required')
166
+ return decision;
167
+ const missing = decision.next_step.missing_scopes ?? [];
168
+ if (missing.some(scope => !input.scopes.includes(scope)))
169
+ fail('permission_required', 'Request every missing permission reported for this action.');
170
+ const existing = this.pairings.get(ws.root);
171
+ if (existing) {
172
+ const result = existing.result();
173
+ if (existing.finished)
174
+ this.pairings.delete(ws.root);
175
+ return { ...result, requested_action: input.action };
176
+ }
177
+ const credential = await this.credential();
178
+ const settings = await new ApiClient(this.options.origin, credential?.token, fetch, this.options.dev).request('GET', '/mcp/settings');
179
+ if (!Array.isArray(settings?.scopes))
180
+ fail('server_update_required', 'Update the SupportPages server before requesting additional permissions.');
181
+ const scopes = [...new Set([...settings.scopes.filter(scope => ['publish', 'manage', 'generate'].includes(scope)), ...input.scopes])];
182
+ const pairing = new Pairing(this.options.origin, this.options.dev, this.options.pairingRuntime);
183
+ const approval = await pairing.start(deviceLabel(), { client: 'SupportPages Writer MCP' });
184
+ this.pairings.set(ws.root, pairing);
185
+ pairing.run(async (delivery) => {
186
+ if (settings.account && delivery.account.id !== settings.account.id)
187
+ fail('account_mismatch', 'Approve permissions using the account already connected to this device.');
188
+ await this.saveCredential(delivery);
189
+ });
190
+ return { ...pairing.result(), verification_uri: approval.url, user_code: approval.code, requested_action: input.action,
191
+ instructions: 'Approve the requested permissions in your browser, then retry the original action. The existing credential is unchanged until consent completes.' };
192
+ }
193
+ async saveCredential(delivery) {
194
+ const o = this.options;
195
+ const { filename } = credentialLocation(o.origin, o.configDir);
196
+ await saveTokenFile(filename, o.origin, delivery.token, delivery.account);
197
+ for (const { bridge } of this.bridges.values())
198
+ await bridge.close();
199
+ this.bridges.clear();
200
+ }
201
+ /** Active setup for an explicitly requested hosted action. Inspection tools
202
+ * never call this: opening setup is not approval and never starts a job. */
203
+ async setupHosted(input) {
204
+ const ws = await this.workspace(input.workspace);
205
+ const key = ws.root;
206
+ const running = this.hostedConnecting.get(key);
207
+ if (running) {
208
+ await running;
209
+ return this.setupHosted(input);
210
+ }
211
+ const work = this.hostedSetup(input, ws);
212
+ this.hostedConnecting.set(key, work);
213
+ try {
214
+ return await work;
215
+ }
216
+ finally {
217
+ this.hostedConnecting.delete(key);
218
+ }
219
+ }
220
+ /** Ask, in the client's own dialog, whether to sign in before a hosted request
221
+ * the device cannot serve; then sign in, choose the help centre and link this
222
+ * folder, all through dialogs. Declining is remembered for this session so the
223
+ * user is not asked again unless they raise it. */
224
+ async offerAccount(ws, input) {
225
+ if (!this.askForm)
226
+ return { status: 'unavailable' };
227
+ // An earlier "not now" silences unprompted offers, never a request the user just made.
228
+ if (input.explicit)
229
+ this.declinedAccount.delete(ws.root);
230
+ else if (this.declinedAccount.has(ws.root))
231
+ return { status: 'declined', asked: false };
232
+ // A coding client caps the whole tool call (60s in Claude Code, and progress
233
+ // notifications do not extend it). Spend that budget on the dialogs and the
234
+ // approval wait, and leave the rest of the flow to the next call.
235
+ const deadline = Date.now() + (this.options.callBudgetMs ?? 50_000);
236
+ const answer = await this.askForm(`${input.need} This device is not signed in to SupportPages.io. How would you like to continue?`, {
237
+ type: 'object', required: ['account'],
238
+ properties: { account: { type: 'string', title: 'SupportPages.io account', enum: ['sign_in', 'create_account', 'not_now'],
239
+ enumNames: ['Sign in to my existing SupportPages.io account', 'Create a free SupportPages.io account', 'Not now — keep working without an account'] } }
240
+ });
241
+ if (!answer)
242
+ return { status: 'unavailable' };
243
+ const choice = answer.action === 'accept' ? answer.content?.account : undefined;
244
+ if (choice !== 'sign_in' && choice !== 'create_account') {
245
+ this.declinedAccount.add(ws.root);
246
+ return { status: 'declined', asked: true };
247
+ }
248
+ const auth = await this.authentication(ws, { host_local: true, signup: choice === 'create_account', open_browser: true,
249
+ await_ms: Math.min(25_000, Math.max(0, deadline - Date.now() - 12_000)) });
250
+ return this.linkAfterSignIn(ws, auth, deadline);
251
+ }
252
+ async linkAfterSignIn(ws, auth, deadline = Date.now() + 20_000) {
253
+ if (auth.status === 'ready')
254
+ return { status: 'linked', project_id: String(auth.project_id), account: auth.account };
255
+ // Too little budget left to ask well: the next call asks instead of risking
256
+ // a dialog the client cancels when the tool call times out.
257
+ if (auth.status !== 'project_required' || !this.askForm || deadline - Date.now() < 8_000)
258
+ return auth;
259
+ const listed = auth.projects;
260
+ const projects = listed?.projects ?? [];
261
+ const answer = await this.askForm('Which help centre should this folder use?', {
262
+ type: 'object', required: ['project'],
263
+ properties: { project: { type: 'string', title: 'Help centre', enum: [...projects.map(project => project.id), 'new'],
264
+ enumNames: [...projects.map(project => project.help_centre_url ? `${project.name} · ${project.help_centre_url}` : project.name), 'Create a new help centre'] } }
265
+ });
266
+ const pick = answer?.action === 'accept' ? answer.content?.project : undefined;
267
+ if (typeof pick !== 'string' || pick !== 'new' && !projects.some(project => project.id === pick))
268
+ return auth;
269
+ const bridge = await this.bridge(ws, { resumeCompletion: false });
270
+ let projectId = pick;
271
+ if (pick === 'new') {
272
+ const suggested = path.basename(ws.root);
273
+ const details = await this.askForm('Name the new help centre.', {
274
+ type: 'object', required: ['name', 'subdomain'],
275
+ properties: { name: { type: 'string', title: 'Help centre name', default: suggested, minLength: 1, maxLength: 100 },
276
+ subdomain: { type: 'string', title: 'Address', description: 'Letters, digits and hyphens: <address>.supportpages.io', default: suggested.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63), minLength: 3, maxLength: 63 } }
277
+ });
278
+ const name = details?.action === 'accept' ? details.content?.name : undefined, subdomain = details?.action === 'accept' ? details.content?.subdomain : undefined;
279
+ if (typeof name !== 'string' || typeof subdomain !== 'string')
280
+ return auth;
281
+ const created = await bridge.createProject(name.trim(), subdomain.trim().toLowerCase());
282
+ projectId = String(created.project?.id ?? created.id);
283
+ }
284
+ await bridge.bind(projectId, { requireIdle: true });
285
+ return { status: 'linked', project_id: projectId, account: auth.account };
286
+ }
287
+ async openSetupPage(url) {
288
+ const target = new URL(url), origin = new URL(this.options.origin);
289
+ if (target.username || target.password || (target.origin !== origin.origin && !(this.options.dev && origin.protocol === 'http:' && target.protocol === 'https:' && target.hostname === origin.hostname)))
290
+ fail('invalid_response', 'The server returned an invalid setup link.');
291
+ let opening = this.setupPages.get(url);
292
+ if (!opening) {
293
+ opening = this.openBrowser(url).catch(() => false);
294
+ this.setupPages.set(url, opening);
295
+ }
296
+ return { browser_opened: await opening, browser_url: url };
297
+ }
298
+ async hostedSetup(input, ws) {
299
+ const next = { workspace: ws.root, action: input.action, ...(input.project_id ? { project_id: input.project_id } : {}), ...(input.article_id ? { article_id: input.article_id } : {}), ...(input.resume_arguments ? { resume_arguments: input.resume_arguments } : {}) };
300
+ const base = { requested_action: input.action, next_tool: 'supportpages_setup_hosted', next_arguments: next, resume_tool: `supportpages_${input.action}`, resume_arguments: { workspace: ws.root, ...input.resume_arguments, ...(input.article_id ? { article_id: input.article_id } : {}) } };
301
+ const authenticate = async () => {
302
+ // With a client dialog the user chooses first; the sign-in that follows
303
+ // also links the folder, so setup simply continues.
304
+ const offered = await this.offerAccount(ws, { need: 'This request runs in SupportPages.io.' });
305
+ if (offered.status === 'linked')
306
+ return this.hostedSetup(input, ws);
307
+ if (offered.status === 'declined')
308
+ return { ...base, status: 'declined', instructions: 'The user chose not to sign in. Do not start setup or ask again unless they raise it.' };
309
+ if (offered.status !== 'unavailable')
310
+ return { ...offered, ...base, instructions: offered.status === 'authentication_required'
311
+ ? 'Sign-in continues in the browser. Show the comparison code and browser_url if browser_opened is false; call supportpages_setup_hosted with next_arguments once approved.'
312
+ : 'Sign-in finished but no help centre was chosen. Ask which help centre to use (or create one), then call supportpages_setup_hosted with next_arguments and its project_id.' };
313
+ const result = await this.authentication(ws, { host_local: true, setup_action: input.action, open_browser: true, signup: input.signup });
314
+ return { ...result, ...base, instructions: result.status === 'authorization_cancelled'
315
+ ? 'Sign-in was cancelled. Retry setup only if the user requests it.'
316
+ : result.status === 'signed_in' || result.status === 'ready'
317
+ ? 'Sign-in is complete. Call supportpages_setup_hosted with next_arguments to continue the original request.'
318
+ : 'First sign in or create an account in the opened browser and approve this device. Show the comparison code and use browser_url as a fallback if browser_opened is false. Do not ask to connect a repository yet. After approval, call supportpages_setup_hosted with next_arguments to continue the original request. Never approve consent for the user.' };
319
+ };
320
+ const pairing = this.pairings.get(ws.root);
321
+ if (pairing) {
322
+ if (!pairing.finished)
323
+ return authenticate();
324
+ this.pairings.delete(ws.root);
325
+ const result = pairing.result();
326
+ if (result.status !== 'ready')
327
+ return { ...result, ...base, instructions: 'Browser consent was cancelled. Retry setup only if the user requests it.' };
328
+ }
329
+ let bridge;
330
+ try {
331
+ bridge = await this.bridge(ws, { resumeCompletion: false });
332
+ if (!bridge.api.configured())
333
+ return authenticate();
334
+ // Validate sign-in before project selection. Never auto-select a project,
335
+ // replace an existing binding, import local articles or run local analysis.
336
+ const projects = await bridge.listProjects();
337
+ if (input.project_id) {
338
+ await bridge.bind(input.project_id, { requireIdle: true });
339
+ delete next.project_id;
340
+ }
341
+ if (await bridge.destination() !== 'hosted')
342
+ return { ...base, status: 'project_required', projects,
343
+ instructions: 'Sign-in is complete. Ask which help centre to use; call supportpages_create_project if the user wants a new one, then call supportpages_setup_hosted with next_arguments and its project_id. Repository setup will open after selection. Do not call local init or start local analysis.' };
344
+ }
345
+ catch (error) {
346
+ if (['invalid_credentials', 'invalid_credentials_file'].includes(publicError(error).code) && !this.options.token && !this.options.tokenFile)
347
+ return authenticate();
348
+ throw error;
349
+ }
350
+ const decision = await resolveAction(bridge, input.action, input.article_id);
351
+ if (decision.next_step?.code === 'authentication_required')
352
+ return authenticate();
353
+ if (decision.next_step?.code === 'permission_required') {
354
+ const scopes = (decision.next_step.missing_scopes ?? []).filter((scope) => ['publish', 'manage', 'generate'].includes(scope));
355
+ const consent = await this.requestPermissions({ workspace: ws.root, action: input.action, article_id: input.article_id, scopes });
356
+ return { ...base, ...consent, ...('verification_uri' in consent && consent.verification_uri ? await this.openSetupPage(consent.verification_uri) : {}),
357
+ instructions: 'Approve the additional device permissions in the opened browser, then call supportpages_setup_hosted with next_arguments. The existing token is unchanged until you approve. Do not approve consent for the user.' };
358
+ }
359
+ return { ...base, ...decision, status: decision.allowed ? 'available' : 'action_required',
360
+ ...(decision.next_step?.url ? await this.openSetupPage(decision.next_step.url) : {}),
361
+ instructions: decision.allowed
362
+ ? 'Setup is ready. Retry the original requested action with its original arguments. For a walkthrough, choose a completed hosted article using supportpages_list_articles with source=hosted if no article_id was supplied. Do not restore deleted local articles or upload one without an explicit request.'
363
+ : `${decision.next_step?.message} ${decision.next_step?.url ? 'The required page has been opened if browser launch is available; otherwise show browser_url for the user to open. Complete this step there, then call supportpages_setup_hosted with next_arguments.' : 'Resolve this prerequisite before retrying.'} Preserve the original request; do not start local generation or unrelated jobs.` };
364
+ }
365
+ /** CLI-only: move this folder to another help centre after the user confirmed it. The device credential is untouched. */
366
+ async switchProject(ws, expectedProjectId, projectId) {
367
+ if (this.options.tokenFile || this.options.token)
368
+ fail('invalid_credentials_file', 'Remove the explicit token override before changing the connected help centre.');
369
+ return replaceConnection(await this.bridge(ws), expectedProjectId, projectId);
370
+ }
371
+ async logout() {
372
+ const o = this.options;
373
+ if (o.tokenFile || o.token)
374
+ fail('invalid_configuration', 'Logout manages the device credential. Remove explicit token overrides and revoke those tokens in SupportPages.io settings.');
375
+ const { filename } = credentialLocation(o.origin, o.configDir);
376
+ const result = { status: 'logged_out', api_origin: o.origin };
377
+ try {
378
+ await lstat(filename);
379
+ }
380
+ catch (error) {
381
+ if (error.code === 'ENOENT')
382
+ return { ...result, already_logged_out: true };
383
+ throw error;
384
+ }
385
+ const token = await readTokenFile(filename, o.origin);
386
+ try {
387
+ const response = await new ApiClient(o.origin, token, fetch, o.dev).request('DELETE', '/mcp/session');
388
+ if (response?.status !== 'logged_out')
389
+ fail('invalid_response', `SupportPages.io did not confirm logout. Retry ${CLI_NAME} logout.`);
390
+ }
391
+ catch (error) {
392
+ // An expired/revoked credential is already unusable; retry after a lost
393
+ // successful response also lands here. Keep credentials on network errors
394
+ // so revocation can be retried rather than leaving an active token behind.
395
+ if (error.code !== 'invalid_credentials')
396
+ throw error;
397
+ }
398
+ // Do not remove a replacement credential saved while revocation was pending.
399
+ try {
400
+ if (await readTokenFile(filename, o.origin) !== token)
401
+ fail('connection_changed', `This device signed in again during logout. Its new credential was kept. Run ${CLI_NAME} logout again to revoke it.`);
402
+ }
403
+ catch (error) {
404
+ try {
405
+ await lstat(filename);
406
+ }
407
+ catch (missing) {
408
+ if (missing.code === 'ENOENT')
409
+ return result;
410
+ }
411
+ throw error;
412
+ }
413
+ for (const { bridge } of this.bridges.values())
414
+ await bridge.close();
415
+ this.bridges.clear();
416
+ await rm(filename, { force: true });
417
+ return result;
418
+ }
419
+ async status(input = {}) {
420
+ const ws = await this.workspace(input.workspace);
421
+ const article_run = await new Runs(ws, connectionStateRoot(this.options.origin, this.options.dev)).progress(input.run_id);
422
+ const account = await this.account();
423
+ const base = { workspace: ws.root, api_origin: this.options.origin, dev_mode: this.options.dev, account, article_run };
424
+ const pairing = this.pairings.get(ws.root);
425
+ if (pairing) {
426
+ // Observing progress must not consume a terminal result or restart pairing.
427
+ try {
428
+ const result = pairing.result();
429
+ if (result.status !== 'ready')
430
+ return { ...base, ...result,
431
+ status: result.status === 'authentication_required' ? 'approval_pending' : result.status,
432
+ instructions: 'Complete the sign-in approval in your browser, then call supportpages_status to check progress. Call supportpages_init to start again after a cancelled request.' };
433
+ }
434
+ catch (error) {
435
+ const safe = publicError(error);
436
+ return { ...base, status: 'authorization_failed', error: safe,
437
+ instructions: 'Call supportpages_init to clear the failed request, then call it again to sign in.' };
438
+ }
439
+ }
440
+ else if (this.initializing.has(ws.root)) {
441
+ return { ...base, status: 'connecting', instructions: 'Initialization is running. Call supportpages_status again shortly.' };
442
+ }
443
+ try {
444
+ return { account, ...(await (await this.bridge(ws)).status(input.run_id)) };
445
+ }
446
+ catch (error) {
447
+ const safe = publicError(error);
448
+ return { ...base, status: safe.code === 'invalid_credentials_file' ? 'authentication_required' : 'connection_error',
449
+ error: safe, instructions: 'The saved device sign-in could not be used. Ask whether the user wants to sign in again, then call supportpages_init.' };
450
+ }
451
+ }
452
+ async authentication(ws, input = {}) {
453
+ if (input.setup_action && (this.options.token || this.options.tokenFile))
454
+ fail('authentication_required', 'Replace the explicitly configured credential or remove the token override before signing in through the browser.');
455
+ const o = this.options;
456
+ const existing = this.pairings.get(ws.root);
457
+ if (existing) {
458
+ if (input.await_ms && !existing.finished)
459
+ await existing.wait(input.await_ms);
460
+ if (existing.finished && existing.result().status === 'ready') {
461
+ this.pairings.delete(ws.root);
462
+ return this.initialize(input, ws);
463
+ }
464
+ try {
465
+ const result = existing.result();
466
+ return { ...result, ...(input.open_browser && result.status === 'authentication_required' && result.verification_uri ? await this.openSetupPage(result.verification_uri) : {}) };
467
+ }
468
+ finally {
469
+ if (existing.finished)
470
+ this.pairings.delete(ws.root);
471
+ }
472
+ }
473
+ const pairing = new Pairing(o.origin, o.dev, o.pairingRuntime);
474
+ const request = await pairing.start(deviceLabel(), { client: 'SupportPages Writer MCP' });
475
+ // The consent page signs in by default; a user who chose to create a free
476
+ // account lands on registration instead (the same switch the terminal uses).
477
+ if (input.signup) {
478
+ const url = new URL(request.url);
479
+ url.searchParams.set('signup', '1');
480
+ request.url = url.href;
481
+ }
482
+ if (this.closed) {
483
+ pairing.cancel();
484
+ return pairing.result();
485
+ }
486
+ this.pairings.set(ws.root, pairing);
487
+ let action = 'unsupported';
488
+ try {
489
+ action = await this.approval?.(request) ?? 'unsupported';
490
+ }
491
+ catch { /* A client without working URL elicitation gets the same safe link. */ }
492
+ if (action === 'decline' || action === 'cancel') {
493
+ pairing.cancel();
494
+ this.pairings.delete(ws.root);
495
+ return pairing.result();
496
+ }
497
+ const browser = input.open_browser ? action === 'accept' ? { browser_opened: true, browser_url: request.url } : await this.openSetupPage(request.url) : {};
498
+ pairing.run(async (delivery) => {
499
+ await this.saveCredential(delivery);
500
+ }, action === 'accept' ? () => this.approvalCompleted?.(request.id) ?? Promise.resolve() : undefined);
501
+ if (action === 'accept' || input.await_ms) {
502
+ // The user is at the approval page now, so hold the call briefly and
503
+ // finish the whole flow rather than handing back a link to poll.
504
+ await pairing.wait(action === 'accept' ? undefined : input.await_ms);
505
+ if (pairing.finished) {
506
+ try {
507
+ pairing.result();
508
+ }
509
+ finally {
510
+ this.pairings.delete(ws.root);
511
+ }
512
+ return input.setup_action ? { status: 'signed_in', ...browser } : this.initialize(input, ws);
513
+ }
514
+ }
515
+ return { ...pairing.result(), workspace: ws.root, api_origin: o.origin, ...browser };
516
+ }
517
+ async init(input) {
518
+ return this.connect(input);
519
+ }
520
+ /** MCP equivalent of the CLI's publish flow. Each response names the next user choice. */
521
+ async publish(input = {}) {
522
+ const ws = await this.workspace(input.workspace);
523
+ const initial = await this.bridge(ws);
524
+ const inventory = await initial.lock(async () => {
525
+ const destination = await initial.destination();
526
+ if (destination === 'none')
527
+ fail('project_required', 'This folder is not set up yet, so there are no saved articles to host. Call supportpages_init to link a help centre, or create an article first.');
528
+ await initial.runs.assertAvailable();
529
+ return { destination, articles: await initial.localArticles() };
530
+ });
531
+ if (!inventory.articles.length && !(inventory.destination === 'hosted' && input.slugs?.length)) {
532
+ if (inventory.destination === 'hosted')
533
+ return { status: 'up_to_date', uploaded: [], instructions: 'Every saved article in this folder is already on SupportPages.io.' };
534
+ fail('nothing_to_publish', 'No saved articles to publish yet. Finish an article first.');
535
+ }
536
+ if (!initial.api.configured()) {
537
+ const offered = await this.offerAccount(ws, { need: 'Hosting these articles needs a SupportPages.io help centre.', explicit: true });
538
+ if (offered.status === 'declined')
539
+ return { status: 'declined', articles: inventory.articles, instructions: 'The user chose not to sign in. No articles were uploaded and the folder keeps saving locally. Do not ask again unless they raise it.' };
540
+ }
541
+ const connection = await this.connect({ ...input, host_local: true });
542
+ if (connection.status !== 'ready')
543
+ return { ...connection, articles: inventory.articles,
544
+ instructions: connection.status === 'project_required'
545
+ ? 'Ask which help centre to use, then call supportpages_publish with project_id and the selected article slugs. Use supportpages_create_project first if the user wants a new help centre.'
546
+ : connection.status === 'authorization_cancelled'
547
+ ? 'Sign-in was cancelled. No articles were uploaded and the folder was not connected. Call supportpages_publish again only if the user wants to retry.'
548
+ : 'Show the browser approval link and comparison code if returned. The user must approve sign-in; never accept credentials in chat. Check supportpages_status, then call supportpages_publish again with the chosen project_id and slugs. No articles have been uploaded.' };
549
+ return (await this.bridge(ws)).uploadLocalArticles(input.slugs);
550
+ }
551
+ async connect(input) {
552
+ const ws = await this.workspace(input.workspace);
553
+ const running = this.initializing.get(ws.root);
554
+ if (running)
555
+ return running;
556
+ const work = this.initWorkspace(input, ws);
557
+ this.initializing.set(ws.root, work);
558
+ try {
559
+ return await work;
560
+ }
561
+ finally {
562
+ this.initializing.delete(ws.root);
563
+ }
564
+ }
565
+ async initWorkspace(input, ws) {
566
+ const pairing = this.pairings.get(ws.root);
567
+ if (pairing) {
568
+ if (!pairing.finished)
569
+ return this.authentication(ws, input);
570
+ this.pairings.delete(ws.root);
571
+ const result = pairing.result();
572
+ if (result.status !== 'ready')
573
+ return result;
574
+ }
575
+ return this.initialize(input, ws);
576
+ }
577
+ async initialize(input, ws) {
578
+ let bridge;
579
+ let projects;
580
+ try {
581
+ bridge = await this.bridge(ws);
582
+ // A folder that saves articles locally needs no sign-in; an explicit
583
+ // project_id still connects it to a help centre.
584
+ if (!input.project_id && !input.host_local && await bridge.destination() === 'local') {
585
+ const local = (await bridge.local());
586
+ const analysis = await bridge.setup.analysis();
587
+ return { status: 'local', workspace: bridge.ws.root, api_origin: this.options.origin, account: await this.account(),
588
+ analysis, generation_ready: analysis.status === 'ready', export_dir: local.export_dir,
589
+ instructions: [analysis.status === 'ready' ? undefined : analysisInstruction(analysis),
590
+ analysis.status === 'ready'
591
+ ? 'This folder saves articles locally as Markdown and screenshots; no help centre or sign-in is needed. Call supportpages_prepare_article to write one. If the user asks to host them or to use a help centre, ask whether to sign in or create a free SupportPages.io account, then call supportpages_init with host_local=true (signup=true for a new account); use supportpages_publish instead when the saved articles should be uploaded as well.'
592
+ : 'This folder saves articles locally.'].filter(Boolean).join(' ') };
593
+ }
594
+ if (bridge.api.configured())
595
+ projects = await bridge.listProjects();
596
+ }
597
+ catch (error) {
598
+ if (['invalid_credentials', 'invalid_credentials_file'].includes(error.code ?? '') && !this.options.tokenFile)
599
+ return this.authentication(ws, input);
600
+ throw error;
601
+ }
602
+ const o = this.options;
603
+ if (!bridge.api.configured())
604
+ return this.authentication(bridge.ws, input);
605
+ if (input.project_id)
606
+ await bridge.bind(input.project_id, { requireIdle: input.host_local });
607
+ const bound = await bridge.ws.exists(`${bridge.stateRoot}/binding.json`) ? await bridge.binding() : undefined;
608
+ if (!bound) {
609
+ return { status: 'project_required', workspace: bridge.ws.root, api_origin: o.origin, account: await this.account(), projects,
610
+ instructions: 'This device is signed in but the folder is not linked to a help centre yet. Ask the user which help centre it should use, then call supportpages_init again with that project_id. Call supportpages_create_project first to create a new help centre.' };
611
+ }
612
+ const analysis = await bridge.setup.analysis();
613
+ const synchronization = await bridge.sync();
614
+ return { status: 'ready', workspace: bridge.ws.root, api_origin: o.origin, account: await this.account(), project_id: bound.project_id,
615
+ analysis, synchronization, generation_ready: analysis.status === 'ready',
616
+ ...(analysis.status !== 'ready' ? { instructions: `The connection is ready. Run ${CLI_NAME} analyse in this project’s terminal before creating an article.` } : {}) };
617
+ }
618
+ }
619
+ //# sourceMappingURL=session.js.map