@rikaidev/yomi 0.2.6

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +81 -0
  3. package/PRIVACY.md +39 -0
  4. package/README.md +659 -0
  5. package/dist/auth/credential-store.js +395 -0
  6. package/dist/auth/keychain.js +256 -0
  7. package/dist/cli/login.js +142 -0
  8. package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
  9. package/dist/line/auth/protocol/index.js +3 -0
  10. package/dist/line/auth/protocol/login-metadata.js +159 -0
  11. package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
  12. package/dist/line/auth/pwless/index.js +163 -0
  13. package/dist/line/auth/pwless/steps.js +213 -0
  14. package/dist/line/client/auth-service/client.js +240 -0
  15. package/dist/line/client/e2ee-service/client.js +69 -0
  16. package/dist/line/client/e2ee-service/requests.js +92 -0
  17. package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
  18. package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
  19. package/dist/line/client/index.js +213 -0
  20. package/dist/line/client/obs-media-client.js +140 -0
  21. package/dist/line/client/parsers.js +69 -0
  22. package/dist/line/client/shop-service/client.js +63 -0
  23. package/dist/line/client/sticker-meta.js +46 -0
  24. package/dist/line/client/sync-service/client.js +317 -0
  25. package/dist/line/client/talk-service/chat-query.js +122 -0
  26. package/dist/line/client/talk-service/command-client.js +242 -0
  27. package/dist/line/client/talk-service/contact-query.js +77 -0
  28. package/dist/line/client/talk-service/message-box-query.js +137 -0
  29. package/dist/line/client/talk-service/message-id-query.js +241 -0
  30. package/dist/line/client/talk-service/query-client.js +219 -0
  31. package/dist/line/client/talk-service/requests.js +383 -0
  32. package/dist/line/client/transport.js +138 -0
  33. package/dist/line/core/auth-session-logout.js +55 -0
  34. package/dist/line/core/auth-session-runtime.js +231 -0
  35. package/dist/line/core/auth-session-service.js +85 -0
  36. package/dist/line/core/chat-runtime-service.js +343 -0
  37. package/dist/line/core/client-runtime.js +17 -0
  38. package/dist/line/core/config.js +67 -0
  39. package/dist/line/core/constants.js +56 -0
  40. package/dist/line/core/contact-query-service.js +20 -0
  41. package/dist/line/core/directory-runtime-service.js +83 -0
  42. package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
  43. package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
  44. package/dist/line/core/e2ee/index.js +6 -0
  45. package/dist/line/core/e2ee/keys/decrypt.js +110 -0
  46. package/dist/line/core/e2ee/keys/encrypt.js +117 -0
  47. package/dist/line/core/e2ee/keys/group-key.js +260 -0
  48. package/dist/line/core/e2ee/keys/key-manager.js +231 -0
  49. package/dist/line/core/e2ee/keys/key-payload.js +130 -0
  50. package/dist/line/core/e2ee/keys/key-types.js +1 -0
  51. package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
  52. package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
  53. package/dist/line/core/e2ee/keys/peer-key.js +191 -0
  54. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
  55. package/dist/line/core/e2ee/types.js +4 -0
  56. package/dist/line/core/group-query-service.js +20 -0
  57. package/dist/line/core/media-decrypt.js +78 -0
  58. package/dist/line/core/media-encrypt.js +97 -0
  59. package/dist/line/core/mention.js +89 -0
  60. package/dist/line/core/message-command-service.js +454 -0
  61. package/dist/line/core/message-query-service.js +161 -0
  62. package/dist/line/core/service.js +134 -0
  63. package/dist/line/core/session-state/index.js +243 -0
  64. package/dist/line/core/session-state/persist.js +192 -0
  65. package/dist/line/core/store.js +8 -0
  66. package/dist/line/core/thrift/binary/binary-reader.js +151 -0
  67. package/dist/line/core/thrift/binary/binary-writer.js +157 -0
  68. package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
  69. package/dist/line/core/thrift/compact/compact-reader.js +180 -0
  70. package/dist/line/core/thrift/compact/compact-writer.js +161 -0
  71. package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
  72. package/dist/line/core/thrift/fields/builders.js +124 -0
  73. package/dist/line/core/thrift/index.js +12 -0
  74. package/dist/line/core/thrift/message/decoder.js +55 -0
  75. package/dist/line/core/thrift/message/encoder.js +25 -0
  76. package/dist/line/core/thrift/primitives/varint.js +64 -0
  77. package/dist/line/core/thrift/struct/reader.js +172 -0
  78. package/dist/line/core/thrift/struct/writer.js +189 -0
  79. package/dist/line/core/thrift/types.js +23 -0
  80. package/dist/line/core/video-thumbnail.js +104 -0
  81. package/dist/mcp/handlers/contacts.js +166 -0
  82. package/dist/mcp/handlers/conversations.js +193 -0
  83. package/dist/mcp/handlers/groups.js +123 -0
  84. package/dist/mcp/handlers/index.js +15 -0
  85. package/dist/mcp/handlers/login.js +473 -0
  86. package/dist/mcp/handlers/media.js +69 -0
  87. package/dist/mcp/handlers/message-actions.js +62 -0
  88. package/dist/mcp/handlers/scope.js +120 -0
  89. package/dist/mcp/handlers/search.js +196 -0
  90. package/dist/mcp/handlers/send.js +523 -0
  91. package/dist/mcp/handlers/shared.js +84 -0
  92. package/dist/mcp/handlers/stickers.js +87 -0
  93. package/dist/mcp/media.js +228 -0
  94. package/dist/mcp/names.js +133 -0
  95. package/dist/mcp/phi-guard.js +105 -0
  96. package/dist/mcp/policy.js +34 -0
  97. package/dist/mcp/server.js +301 -0
  98. package/dist/mcp/tools.js +761 -0
  99. package/dist/mcp/ui/capability.js +26 -0
  100. package/dist/mcp/ui/login-app-protocol.js +118 -0
  101. package/dist/mcp/ui/login-app-styles.js +142 -0
  102. package/dist/mcp/ui/login-app-view.js +361 -0
  103. package/dist/mcp/ui/login-app.js +72 -0
  104. package/dist/mcp/ui/login-structured-content.js +1 -0
  105. package/dist/mcp/ui/resource.js +35 -0
  106. package/dist/mcp/ui/tools-with-ui.js +40 -0
  107. package/dist/search/bigram.js +86 -0
  108. package/dist/search/capture.js +140 -0
  109. package/dist/search/collector.js +176 -0
  110. package/dist/search/default-embedder.js +101 -0
  111. package/dist/search/embedder.js +13 -0
  112. package/dist/search/scope.js +142 -0
  113. package/dist/search/sqlite.js +141 -0
  114. package/dist/search/store.js +469 -0
  115. package/dist/util/data-dir.js +55 -0
  116. package/dist/util/log.js +86 -0
  117. package/dist/version.js +7 -0
  118. package/package.json +63 -0
  119. package/run.mjs +71 -0
@@ -0,0 +1,26 @@
1
+ /** The MCP Apps UI mime type the host must declare support for. */
2
+ export const MCP_APPS_MIME_TYPE = 'text/html;profile=mcp-app';
3
+ /** The extension key the MCP Apps spec registers capability support under. */
4
+ const MCP_APPS_EXTENSION_KEY = 'io.modelcontextprotocol/ui';
5
+ /**
6
+ * Does the connected client's negotiated capabilities include MCP Apps UI
7
+ * support (the `text/html;profile=mcp-app` mime type under the
8
+ * `io.modelcontextprotocol/ui` extension)?
9
+ *
10
+ * @param capabilities - Result of `server.getClientCapabilities()`.
11
+ * @returns True only when the client explicitly declared the mime type.
12
+ */
13
+ export function supportsMcpApps(capabilities) {
14
+ const extension = capabilities?.extensions?.[MCP_APPS_EXTENSION_KEY];
15
+ if (!extension || typeof extension !== 'object') {
16
+ return false;
17
+ }
18
+ if (!('mimeTypes' in extension)) {
19
+ return false;
20
+ }
21
+ const { mimeTypes } = extension;
22
+ if (!Array.isArray(mimeTypes)) {
23
+ return false;
24
+ }
25
+ return mimeTypes.includes(MCP_APPS_MIME_TYPE);
26
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * JSON-RPC-over-postMessage transport for the login view, plus the MCP
3
+ * Apps lifecycle handshake (`ui/initialize` -> `ui/notifications/initialized`)
4
+ * and size-changed reporting. Emitted verbatim into ../login-app.ts's
5
+ * `<script>` tag, ahead of ./login-app-view.ts's rendering code, inside one
6
+ * shared `(function () { 'use strict'; ... })();` wrapper both blobs are
7
+ * concatenated into — so the `var`/`function` declarations here are plain
8
+ * ordinary bindings, visible to login-app-view.ts by simple scope, not
9
+ * exported through any `window.*` global. Split out of login-app.ts purely
10
+ * to keep that file (and this one) under the project's 500-scc-line module
11
+ * cap.
12
+ *
13
+ * Per the MCP Apps spec (specification/2026-01-26/apps.mdx), the JSON-RPC
14
+ * message IS the postMessage payload (no envelope), sent to `window.parent`
15
+ * with targetOrigin `'*'` — the view must not perform origin checks; it
16
+ * trusts `window.parent`. ONE `message` listener demultiplexes both
17
+ * directions: a message carrying `id` plus `result`/`error` is a response
18
+ * to a request THIS view sent (`sendRequest`/`callTool`); a message
19
+ * carrying only `method` is a notification FROM the host (`onNotification`
20
+ * subscribers). The spec's own reference snippet sets up one listener per
21
+ * outstanding request, which leaks a listener per call — this file adds
22
+ * exactly one, for the page's lifetime, and correlates by `id` instead.
23
+ */
24
+ /**
25
+ * Build the transport/handshake JS, parameterized by yomi's own version so
26
+ * `ui/initialize`'s `clientInfo.version` reports it.
27
+ *
28
+ * @param version - `YOMI_VERSION` from ../../version.ts, a build-time
29
+ * constant (not a runtime package.json read — that fails inside a `bun
30
+ * build --compile` binary's virtual filesystem). version.test.ts asserts
31
+ * it stays equal to package.json's "version" field so the two can't drift.
32
+ */
33
+ export function buildLoginAppProtocolJs(version) {
34
+ const versionLiteral = JSON.stringify(version);
35
+ return `
36
+ var nextRequestId = 1;
37
+ var pendingRequests = {};
38
+ var notificationHandlers = {};
39
+
40
+ function onNotification(method, handler) {
41
+ notificationHandlers[method] = handler;
42
+ }
43
+
44
+ function sendRequest(method, params) {
45
+ var id = nextRequestId++;
46
+ return new Promise(function (resolve, reject) {
47
+ pendingRequests[id] = { resolve: resolve, reject: reject };
48
+ window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
49
+ });
50
+ }
51
+
52
+ function sendNotification(method, params) {
53
+ window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*');
54
+ }
55
+
56
+ function callTool(name, toolArguments) {
57
+ return sendRequest('tools/call', { name: name, arguments: toolArguments || {} });
58
+ }
59
+
60
+ window.addEventListener('message', function (event) {
61
+ var message = event.data;
62
+ if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return;
63
+ var hasId = Object.prototype.hasOwnProperty.call(message, 'id');
64
+ var isResponse = hasId && (Object.prototype.hasOwnProperty.call(message, 'result')
65
+ || Object.prototype.hasOwnProperty.call(message, 'error'));
66
+ if (isResponse) {
67
+ var waiting = pendingRequests[message.id];
68
+ if (!waiting) return;
69
+ delete pendingRequests[message.id];
70
+ if (message.error) {
71
+ waiting.reject(new Error((message.error && message.error.message) || 'Request failed.'));
72
+ }
73
+ else {
74
+ waiting.resolve(message.result);
75
+ }
76
+ return;
77
+ }
78
+ if (typeof message.method === 'string') {
79
+ var handler = notificationHandlers[message.method];
80
+ if (handler) handler(message.params || {});
81
+ }
82
+ });
83
+
84
+ var lastReportedSize = null;
85
+
86
+ function reportSize() {
87
+ var width = document.documentElement.scrollWidth;
88
+ var height = document.documentElement.scrollHeight;
89
+ var key = width + 'x' + height;
90
+ if (key === lastReportedSize) return;
91
+ lastReportedSize = key;
92
+ sendNotification('ui/notifications/size-changed', { width: width, height: height });
93
+ }
94
+
95
+ function applyTheme(theme) {
96
+ var root = document.documentElement;
97
+ root.classList.remove('theme-dark', 'theme-light');
98
+ if (theme === 'dark') root.classList.add('theme-dark');
99
+ else if (theme === 'light') root.classList.add('theme-light');
100
+ }
101
+
102
+ function initializeHost(onReady) {
103
+ sendRequest('ui/initialize', {
104
+ capabilities: {},
105
+ clientInfo: { name: 'yomi login', version: ${versionLiteral} },
106
+ protocolVersion: '2026-01-26',
107
+ appCapabilities: { availableDisplayModes: ['inline'] },
108
+ }).then(function (result) {
109
+ var hostContext = (result && result.hostContext) || {};
110
+ applyTheme(hostContext.theme || null);
111
+ sendNotification('ui/notifications/initialized', {});
112
+ onReady(hostContext, null);
113
+ }).catch(function (error) {
114
+ onReady(null, error);
115
+ });
116
+ }
117
+ `;
118
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * CSS for the login view (../login-app.ts's `<style>` block). Split out
3
+ * purely to keep login-app.ts under the project's 500-scc-line module cap
4
+ * as this view grew from a read-only display into one that also renders a
5
+ * credentials form and drives its own tool calls.
6
+ *
7
+ * Color scheme: `prefers-color-scheme` is the fallback (kept from before
8
+ * this file existed), but `html.theme-dark` / `html.theme-light` — applied
9
+ * by ./login-app-protocol.ts from the host's `hostContext.theme` once
10
+ * `ui/initialize` resolves — take precedence via ordinary selector
11
+ * specificity (a class selector always outranks a media-qualified type
12
+ * selector), so an authoritative host theme always wins over the guess.
13
+ */
14
+ export const LOGIN_APP_CSS = `
15
+ :root { color-scheme: light dark; }
16
+ * { box-sizing: border-box; }
17
+ html, body {
18
+ margin: 0;
19
+ padding: 0;
20
+ background: transparent;
21
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
22
+ color: #1c1c1e;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ html, body { color: #ececec; }
26
+ }
27
+ html.theme-dark, html.theme-dark body { color: #ececec; }
28
+ html.theme-light, html.theme-light body { color: #1c1c1e; }
29
+ main {
30
+ padding: 20px 24px;
31
+ max-width: 420px;
32
+ margin: 0 auto;
33
+ }
34
+ .prereq {
35
+ font-size: 12px;
36
+ opacity: 0.65;
37
+ line-height: 1.5;
38
+ margin: 0 0 16px;
39
+ padding-bottom: 12px;
40
+ border-bottom: 1px solid rgba(127, 127, 127, 0.18);
41
+ }
42
+ .label {
43
+ font-size: 12px;
44
+ letter-spacing: 0.06em;
45
+ text-transform: uppercase;
46
+ opacity: 0.6;
47
+ margin: 0 0 8px;
48
+ }
49
+ .pin {
50
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
51
+ font-size: 40px;
52
+ font-weight: 600;
53
+ letter-spacing: 0.12em;
54
+ user-select: text;
55
+ -webkit-user-select: text;
56
+ padding: 14px 0;
57
+ margin: 0 0 20px;
58
+ border-radius: 12px;
59
+ text-align: center;
60
+ background: rgba(127, 127, 127, 0.12);
61
+ }
62
+ .steps {
63
+ margin: 0 0 20px;
64
+ padding-left: 20px;
65
+ line-height: 1.6;
66
+ }
67
+ .steps li { margin-bottom: 6px; }
68
+ .status {
69
+ font-size: 14px;
70
+ opacity: 0.85;
71
+ padding: 10px 14px;
72
+ border-radius: 10px;
73
+ background: rgba(127, 127, 127, 0.10);
74
+ margin: 0 0 12px;
75
+ }
76
+ .idle {
77
+ font-size: 13px;
78
+ opacity: 0.55;
79
+ margin: 0;
80
+ }
81
+ .countdown {
82
+ font-variant-numeric: tabular-nums;
83
+ font-weight: 600;
84
+ }
85
+ .note {
86
+ font-size: 12px;
87
+ opacity: 0.6;
88
+ line-height: 1.5;
89
+ margin: 16px 0 0;
90
+ }
91
+ .sources {
92
+ font-size: 11px;
93
+ opacity: 0.45;
94
+ line-height: 1.6;
95
+ margin: 20px 0 0;
96
+ padding-top: 12px;
97
+ border-top: 1px solid rgba(127, 127, 127, 0.18);
98
+ word-break: break-all;
99
+ }
100
+ .error {
101
+ font-size: 14px;
102
+ padding: 12px 14px;
103
+ border-radius: 10px;
104
+ background: rgba(127, 127, 127, 0.14);
105
+ }
106
+ .form-field {
107
+ margin: 0 0 14px;
108
+ }
109
+ .form-field label {
110
+ display: block;
111
+ font-size: 12px;
112
+ opacity: 0.65;
113
+ margin: 0 0 6px;
114
+ }
115
+ .form-field input,
116
+ .form-field select {
117
+ width: 100%;
118
+ padding: 10px 12px;
119
+ border-radius: 8px;
120
+ border: 1px solid rgba(127, 127, 127, 0.3);
121
+ background: transparent;
122
+ color: inherit;
123
+ font-size: 14px;
124
+ font-family: inherit;
125
+ }
126
+ button.primary {
127
+ width: 100%;
128
+ padding: 12px;
129
+ border: none;
130
+ border-radius: 10px;
131
+ background: rgba(127, 127, 127, 0.16);
132
+ color: inherit;
133
+ font-size: 14px;
134
+ font-weight: 600;
135
+ font-family: inherit;
136
+ cursor: pointer;
137
+ }
138
+ button.primary:disabled {
139
+ opacity: 0.6;
140
+ cursor: default;
141
+ }
142
+ `;
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Rendering + orchestration for the login view (../login-app.ts's
3
+ * `<script>` tag, concatenated after ./login-app-protocol.ts inside the
4
+ * same wrapping IIFE — see that file's header for why this can call
5
+ * `onNotification`/`callTool`/`reportSize`/`initializeHost` directly with
6
+ * no import). Split out of login-app.ts purely to keep that file (and this
7
+ * one) under the project's 500-scc-line module cap.
8
+ *
9
+ * Renders five states off one `stage`/outcome: `need_credentials` (a login
10
+ * form, submitted via `callTool('login', …)` — a HUMAN-initiated action),
11
+ * `pin` (a PIN was issued), `cert` (a stored certificate skipped the PIN
12
+ * step), `error`, and a plain cancelled state.
13
+ *
14
+ * This view has never been observed to render on any deployed MCP client —
15
+ * see ../login-app.ts's header for the upstream host bug tracking this. The
16
+ * MODEL, not this view, is always the driver of login completion
17
+ * (../handlers-login.ts always tells it to call `login_complete` itself).
18
+ * Accordingly `pin`/`cert` are display-only: they show the PIN/steps and a
19
+ * note that completion is in progress elsewhere, and do NOT call
20
+ * `login_complete` themselves — a prior version did, which raced the
21
+ * model's own call against this view's for the same `pendingLogin`. There
22
+ * is now exactly one driver.
23
+ *
24
+ * The `success` stage/renderer was NOT kept as a distinct visual (no "Signed
25
+ * in as …" card): this view no longer calls `login_complete` itself, so it
26
+ * has no business rendering that result. `interpretToolResult`'s fallback
27
+ * branch (a `login_complete`-shaped `{ loggedIn, mid, displayName }` JSON
28
+ * with no `structuredContent`) is technically still reachable — the host
29
+ * could in principle forward that tool-result notification even though this
30
+ * view never initiated the call — so it is handled, but only with a plain,
31
+ * non-optimistic `renderDone()` acknowledgement, never a profile card built
32
+ * from data this view did not ask for.
33
+ *
34
+ * The PIN and any error message are untrusted server/host data — written
35
+ * via `textContent`, never `innerHTML`; nothing interpolates into an HTML
36
+ * string anywhere below. All timing numbers arrive via `structuredContent`
37
+ * at render time — none are written into this source.
38
+ */
39
+ export const LOGIN_APP_VIEW_JS = `
40
+ var SOURCE_A = 'help.line.me/line/IOSSecondary/?contentId=20018574&lang=zh-Hant';
41
+ var SOURCE_B = 'help.line.me/line/smartphone/pc?lang=zh-Hant&contentId=20000112';
42
+
43
+ var app = document.getElementById('app');
44
+
45
+ function clear(container) {
46
+ while (container.firstChild) container.removeChild(container.firstChild);
47
+ return container;
48
+ }
49
+
50
+ function el(tag, className, text) {
51
+ var node = document.createElement(tag);
52
+ if (className) node.className = className;
53
+ if (text !== undefined && text !== null) node.textContent = text;
54
+ return node;
55
+ }
56
+
57
+ function appendPrereq(container) {
58
+ container.appendChild(el('p', 'prereq',
59
+ 'Requires: on the primary phone, 設定 > 我的帳號 > 允許自其他裝置登入 must be enabled, '
60
+ + 'or LINE will not offer this device a sign-in prompt at all.'));
61
+ }
62
+
63
+ function appendSources(container) {
64
+ var sources = el('p', 'sources', null);
65
+ sources.appendChild(document.createTextNode('Sources: ' + SOURCE_A));
66
+ sources.appendChild(document.createElement('br'));
67
+ sources.appendChild(document.createTextNode(SOURCE_B));
68
+ container.appendChild(sources);
69
+ }
70
+
71
+ var countdownTimer = null;
72
+
73
+ function stopCountdown() {
74
+ if (countdownTimer) {
75
+ clearInterval(countdownTimer);
76
+ countdownTimer = null;
77
+ }
78
+ }
79
+
80
+ function renderCountdown(target, totalSeconds, onExpire) {
81
+ stopCountdown();
82
+ var deadline = Date.now() + totalSeconds * 1000;
83
+ function tick() {
84
+ var remaining = Math.max(0, Math.round((deadline - Date.now()) / 1000));
85
+ target.textContent = remaining + 's';
86
+ if (remaining <= 0) {
87
+ stopCountdown();
88
+ onExpire();
89
+ }
90
+ }
91
+ tick();
92
+ countdownTimer = setInterval(tick, 500);
93
+ }
94
+
95
+ function afterRender() {
96
+ reportSize();
97
+ }
98
+
99
+ function renderIdle() {
100
+ stopCountdown();
101
+ var container = clear(app);
102
+ container.appendChild(el('p', 'idle', 'Waiting for the host.'));
103
+ afterRender();
104
+ }
105
+
106
+ function renderError(message) {
107
+ stopCountdown();
108
+ var container = clear(app);
109
+ container.appendChild(el('p', 'error', message || 'Login failed.'));
110
+ afterRender();
111
+ }
112
+
113
+ function renderCancelled() {
114
+ stopCountdown();
115
+ var container = clear(app);
116
+ container.appendChild(el('p', 'status', 'Login was cancelled.'));
117
+ afterRender();
118
+ }
119
+
120
+ // No dedicated "success" renderer: this view is never the driver of login
121
+ // completion (the model always is, per ../handlers-login.ts), so it has
122
+ // no business rendering a signed-in profile. If a \`login_complete\`
123
+ // tool-result notification ever reaches this view anyway (see
124
+ // interpretToolResult below), it gets this same plain acknowledgement,
125
+ // not a fabricated success card built from data this view did not ask for.
126
+ function renderDone() {
127
+ stopCountdown();
128
+ var container = clear(app);
129
+ container.appendChild(el('p', 'status', 'Sign-in finished. See the conversation for details.'));
130
+ afterRender();
131
+ }
132
+
133
+ function renderNeedCredentials(data) {
134
+ stopCountdown();
135
+ var container = clear(app);
136
+
137
+ appendPrereq(container);
138
+ container.appendChild(el('p', 'label', 'Sign in to LINE'));
139
+
140
+ var form = document.createElement('form');
141
+
142
+ var phoneField = el('div', 'form-field');
143
+ phoneField.appendChild(el('label', null, 'Phone number (E.164, e.g. +8869XXXXXXXX)'));
144
+ var phoneInput = document.createElement('input');
145
+ phoneInput.type = 'tel';
146
+ phoneInput.required = true;
147
+ phoneInput.placeholder = '+8869XXXXXXXX';
148
+ phoneField.appendChild(phoneInput);
149
+ form.appendChild(phoneField);
150
+
151
+ var regionField = el('div', 'form-field');
152
+ regionField.appendChild(el('label', null, 'Region'));
153
+ var regionSelect = document.createElement('select');
154
+ var regions = Array.isArray(data.regions) ? data.regions : [];
155
+ for (var i = 0; i < regions.length; i++) {
156
+ var option = document.createElement('option');
157
+ option.value = regions[i];
158
+ option.textContent = regions[i];
159
+ regionSelect.appendChild(option);
160
+ }
161
+ regionField.appendChild(regionSelect);
162
+ form.appendChild(regionField);
163
+
164
+ var submitButton = document.createElement('button');
165
+ submitButton.type = 'submit';
166
+ submitButton.className = 'primary';
167
+ submitButton.textContent = 'Continue';
168
+ form.appendChild(submitButton);
169
+ container.appendChild(form);
170
+
171
+ var formStatus = el('p', 'status', null);
172
+ formStatus.style.display = 'none';
173
+ container.appendChild(formStatus);
174
+
175
+ form.addEventListener('submit', function (event) {
176
+ event.preventDefault();
177
+ submitButton.disabled = true;
178
+ formStatus.style.display = '';
179
+ formStatus.textContent = 'Starting login…';
180
+ callTool('login', { phone: phoneInput.value, region: regionSelect.value }).then(function (result) {
181
+ interpretToolResult(result);
182
+ }).catch(function (error) {
183
+ renderError((error && error.message) || 'Login failed.');
184
+ });
185
+ });
186
+
187
+ appendSources(container);
188
+ afterRender();
189
+ }
190
+
191
+ function renderPinStage(data) {
192
+ stopCountdown();
193
+ var container = clear(app);
194
+
195
+ appendPrereq(container);
196
+
197
+ container.appendChild(el('p', 'label', 'LINE login PIN'));
198
+ container.appendChild(el('p', 'pin', data.pin || ''));
199
+
200
+ var steps = el('ol', 'steps');
201
+ steps.appendChild(el('li', null, 'Open LINE on your primary phone.'));
202
+ steps.appendChild(el('li', null, 'Enter the PIN shown above.'));
203
+ steps.appendChild(el('li', null, 'Tick the device that is signing in.'));
204
+ steps.appendChild(el('li', null, 'Tap 「用戶確認」.'));
205
+ steps.appendChild(el('li', null, 'Approve the new device when the primary phone asks.'));
206
+ container.appendChild(steps);
207
+
208
+ var pinStatus = el('p', 'status');
209
+ var pinLabel = document.createTextNode('Time left to enter the code: ');
210
+ var pinCount = el('span', 'countdown');
211
+ pinStatus.appendChild(pinLabel);
212
+ pinStatus.appendChild(pinCount);
213
+ container.appendChild(pinStatus);
214
+
215
+ var clientPinMinutes = Math.round(data.clientPinPollCeilingSeconds / 60);
216
+ container.appendChild(el('p', 'note',
217
+ 'yomi keeps listening for about ' + clientPinMinutes + ' minutes, so a slow phone is not a '
218
+ + 'problem — the code\\'s own 3-minute limit above is the real deadline.'));
219
+ container.appendChild(el('p', 'note',
220
+ 'This PIN step is skipped on future logins once a login certificate has been stored.'));
221
+
222
+ var pinSeconds = data.pinCodeLifetimeSeconds;
223
+ renderCountdown(pinCount, pinSeconds, function () {
224
+ pinStatus.replaceChild(document.createTextNode('The code has expired. '), pinLabel);
225
+ pinCount.textContent = '';
226
+ container.appendChild(el('p', 'error', 'This code is no longer valid. Call login again to get a new one.'));
227
+ afterRender();
228
+ });
229
+
230
+ container.appendChild(el('p', 'note', 'The assistant is completing this sign-in; the result will appear in the conversation.'));
231
+
232
+ appendSources(container);
233
+ afterRender();
234
+ }
235
+
236
+ function renderCertStage(data) {
237
+ stopCountdown();
238
+ var container = clear(app);
239
+
240
+ appendPrereq(container);
241
+
242
+ container.appendChild(el('p', 'label', 'Device approval'));
243
+ container.appendChild(el('p', 'status',
244
+ 'No PIN is needed — a stored login certificate from a previous login was recognised.'));
245
+
246
+ var steps = el('ol', 'steps');
247
+ steps.appendChild(el('li', null, 'Open LINE on your primary phone.'));
248
+ steps.appendChild(el('li', null, 'Approve the new device when it asks.'));
249
+ container.appendChild(steps);
250
+
251
+ var approvalStatus = el('p', 'status');
252
+ var approvalLabel = document.createTextNode('yomi is waiting for approval: ');
253
+ var approvalCount = el('span', 'countdown');
254
+ approvalStatus.appendChild(approvalLabel);
255
+ approvalStatus.appendChild(approvalCount);
256
+ container.appendChild(approvalStatus);
257
+
258
+ var approvalSeconds = data.clientApprovalPollCeilingSeconds;
259
+ renderCountdown(approvalCount, approvalSeconds, function () {
260
+ approvalStatus.replaceChild(document.createTextNode('yomi has stopped waiting for approval. '), approvalLabel);
261
+ approvalCount.textContent = '';
262
+ });
263
+
264
+ container.appendChild(el('p', 'note', 'The assistant is completing this sign-in; the result will appear in the conversation.'));
265
+
266
+ appendSources(container);
267
+ afterRender();
268
+ }
269
+
270
+ function extractText(content) {
271
+ if (!Array.isArray(content)) return null;
272
+ for (var i = 0; i < content.length; i++) {
273
+ if (content[i] && content[i].type === 'text' && typeof content[i].text === 'string') {
274
+ return content[i].text;
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+
280
+ function parseJsonText(text) {
281
+ if (typeof text !== 'string') return null;
282
+ try {
283
+ return JSON.parse(text);
284
+ }
285
+ catch (error) {
286
+ return null;
287
+ }
288
+ }
289
+
290
+ function renderFromStage(data) {
291
+ if (data.stage === 'need_credentials') renderNeedCredentials(data);
292
+ else if (data.stage === 'pin') renderPinStage(data);
293
+ else if (data.stage === 'cert') renderCertStage(data);
294
+ else renderError(typeof data.message === 'string' ? data.message : 'Login failed.');
295
+ }
296
+
297
+ /**
298
+ * Interpret one CallToolResult-shaped object (from either a
299
+ * \`ui/notifications/tool-result\` notification or the direct response
300
+ * to this view's own \`tools/call\`) and render accordingly.
301
+ */
302
+ function interpretToolResult(toolResult) {
303
+ if (!toolResult || typeof toolResult !== 'object') {
304
+ renderError('No result received.');
305
+ return;
306
+ }
307
+ if (toolResult.isError) {
308
+ renderError(extractText(toolResult.content) || 'The tool reported an error.');
309
+ return;
310
+ }
311
+ if (toolResult.structuredContent && typeof toolResult.structuredContent === 'object') {
312
+ renderFromStage(toolResult.structuredContent);
313
+ return;
314
+ }
315
+ // login_complete's success result has no structuredContent — its JSON
316
+ // ({ loggedIn, mid, displayName }) is in content[0].text. This view is
317
+ // never the one that called login_complete (the model always is), so it
318
+ // only ever sees this if the host independently forwards that result —
319
+ // acknowledge it plainly rather than build a success card from it.
320
+ var parsed = parseJsonText(extractText(toolResult.content));
321
+ if (parsed && parsed.loggedIn) {
322
+ renderDone();
323
+ return;
324
+ }
325
+ renderError('Unrecognized tool result.');
326
+ }
327
+
328
+ /**
329
+ * A \`ui/notifications/tool-result\` notification's \`params\` may carry
330
+ * the CallToolResult directly, or nested under a \`result\`/\`toolResult\`/
331
+ * \`output\` wrapper — the spec does not pin down one exact shape, so this
332
+ * checks for either without assuming.
333
+ */
334
+ function unwrapToolResult(params) {
335
+ if (!params || typeof params !== 'object') return null;
336
+ if (params.content || params.structuredContent || params.isError !== undefined) return params;
337
+ var candidates = [params.result, params.toolResult, params.output];
338
+ for (var i = 0; i < candidates.length; i++) {
339
+ if (candidates[i] && typeof candidates[i] === 'object') return candidates[i];
340
+ }
341
+ return null;
342
+ }
343
+
344
+ onNotification('ui/notifications/tool-result', function (params) {
345
+ interpretToolResult(unwrapToolResult(params));
346
+ });
347
+
348
+ onNotification('ui/notifications/tool-cancelled', function () {
349
+ renderCancelled();
350
+ });
351
+
352
+ renderIdle();
353
+ initializeHost(function (hostContext, error) {
354
+ if (error) {
355
+ renderError('Could not connect to host: ' + ((error && error.message) || String(error)));
356
+ }
357
+ // Otherwise stay idle — the next thing to arrive is
358
+ // ui/notifications/tool-input(-partial) (ignored, carries only the call's
359
+ // arguments) followed by tool-result, which drives the first real render.
360
+ });
361
+ `;