dsh-grok-provider 0.1.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.
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.en.md +200 -0
- package/README.md +200 -0
- package/SECURITY.md +35 -0
- package/dist/client/client.js +215 -0
- package/dist/host/index.mjs +147 -0
- package/dist/internal/account-dashboard.mjs +67 -0
- package/dist/internal/auth-controller.mjs +184 -0
- package/dist/internal/auth-registry.mjs +64 -0
- package/dist/internal/auth-rpc.mjs +118 -0
- package/dist/internal/billing-summary.mjs +105 -0
- package/dist/internal/credential-source.mjs +168 -0
- package/dist/internal/grok-adapter.mjs +160 -0
- package/dist/internal/grok-command-handler.mjs +62 -0
- package/dist/internal/grok-command.mjs +17 -0
- package/dist/internal/grok-transport.mjs +279 -0
- package/dist/internal/model-catalog.mjs +142 -0
- package/dist/internal/official-auth-driver.mjs +40 -0
- package/dist/internal/official-cli-auth.mjs +231 -0
- package/dist/internal/official-cli-verifier.mjs +62 -0
- package/dist/internal/official-credential-loader.mjs +64 -0
- package/dist/internal/provider-runtime.mjs +43 -0
- package/dist/internal/responses-codec.mjs +357 -0
- package/dist/internal/responses-request.mjs +246 -0
- package/dist/internal/responses-sse.mjs +123 -0
- package/docs/01-product-requirements.md +128 -0
- package/docs/02-architecture-options.md +180 -0
- package/docs/03-security-threat-model.md +243 -0
- package/docs/04-harness-contract.md +325 -0
- package/docs/05-test-plan.md +224 -0
- package/docs/06-release-plan.md +182 -0
- package/docs/07-decision-gate.md +76 -0
- package/docs/08-upstream-cli-1.0.5-evidence.md +98 -0
- package/docs/09-implementation-status.md +37 -0
- package/docs/README.md +60 -0
- package/docs/adr/0001-auth-and-transport-route.md +86 -0
- package/docs/adr/0002-v0.1-scope.md +46 -0
- package/docs/adr/0003-dual-authentication.md +77 -0
- package/docs/adr/0004-dynamic-model-catalog.md +34 -0
- package/docs/adr/0005-official-cli-only-authentication.md +36 -0
- package/docs/adr/0006-account-dashboard.md +84 -0
- package/grok-provider.patch.yml +3 -0
- package/package.json +92 -0
- package/types/index.d.ts +9 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* global document, window */
|
|
2
|
+
|
|
3
|
+
window.__ModuleLoader__.load({
|
|
4
|
+
id: "dsh-grok-provider",
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
const module = { exports: {} }
|
|
7
|
+
const React = require("react")
|
|
8
|
+
const namespace = "settings.grok"
|
|
9
|
+
const dictionaries = {
|
|
10
|
+
zh: {
|
|
11
|
+
nav: "Grok Build", title: "Grok Build",
|
|
12
|
+
description: "管理官方 Grok Build 登录、账户额度与当前账号可用模型。敏感凭据不会发送到设置页面。",
|
|
13
|
+
loopback: "账户管理只允许在本机页面使用。", loading: "正在读取账户状态…", unavailable: "账户服务暂时不可用。",
|
|
14
|
+
ready: "已登录", notReady: "未登录", driverMissing: "官方 CLI 登录能力不可用",
|
|
15
|
+
running: "登录进行中", succeeded: "登录成功", failed: "登录失败", cancelled: "登录已取消",
|
|
16
|
+
login: "通过浏览器登录", cancel: "取消登录", logout: "退出登录", confirmLogout: "再次点击确认退出",
|
|
17
|
+
officialBody: "登录与凭据刷新由本机官方 Grok CLI 完成。退出会同时影响使用同一 Grok 凭据的其他应用。",
|
|
18
|
+
usage: "使用额度", grokBuild: "Grok Build", refresh: "刷新", refreshing: "刷新中…",
|
|
19
|
+
weekly: "每周额度", monthly: "每月额度", currentPeriod: "当前额度周期",
|
|
20
|
+
remaining: "剩余", used: "已使用", resetTime: "重置时间", resetUnknown: "上游未提供重置时间",
|
|
21
|
+
usageUnknown: "上游未提供使用比例", quotaUnavailable: "暂时无法读取 Grok Build 额度。",
|
|
22
|
+
modelsTitle: "当前账号可用的模型", modelsDescription: "模型来自 Grok Build 动态目录;Harness 模型选择器会显示这里列出的全部模型。",
|
|
23
|
+
modelsUnavailable: "暂时无法读取模型目录。", noModels: "当前账号没有返回可用模型。",
|
|
24
|
+
context: "上下文", reasoning: "推理档位", defaultEffort: "默认", text: "文本输入", streaming: "流式输出", tools: "工具调用",
|
|
25
|
+
lastUpdated: "数据更新时间",
|
|
26
|
+
},
|
|
27
|
+
en: {
|
|
28
|
+
nav: "Grok Build", title: "Grok Build",
|
|
29
|
+
description: "Manage official Grok Build sign-in, account quota, and models available to this account. Sensitive credentials never reach this page.",
|
|
30
|
+
loopback: "Account management is available only from the local page.", loading: "Reading account status…", unavailable: "The account service is temporarily unavailable.",
|
|
31
|
+
ready: "Signed in", notReady: "Signed out", driverMissing: "Official CLI sign-in unavailable",
|
|
32
|
+
running: "Sign-in in progress", succeeded: "Signed in", failed: "Sign-in failed", cancelled: "Sign-in cancelled",
|
|
33
|
+
login: "Sign in with browser", cancel: "Cancel sign-in", logout: "Sign out", confirmLogout: "Click again to confirm sign-out",
|
|
34
|
+
officialBody: "The official local Grok CLI owns sign-in and credential refresh. Signing out also affects other apps sharing the same Grok credential.",
|
|
35
|
+
usage: "Usage quota", grokBuild: "Grok Build", refresh: "Refresh", refreshing: "Refreshing…",
|
|
36
|
+
weekly: "Weekly quota", monthly: "Monthly quota", currentPeriod: "Current quota period",
|
|
37
|
+
remaining: "remaining", used: "Used", resetTime: "Resets", resetUnknown: "Reset time was not provided",
|
|
38
|
+
usageUnknown: "Usage percentage was not provided", quotaUnavailable: "Grok Build quota is temporarily unavailable.",
|
|
39
|
+
modelsTitle: "Models available to this account", modelsDescription: "Models come from the live Grok Build catalog; every model listed here remains visible in the Harness model selector.",
|
|
40
|
+
modelsUnavailable: "The model catalog is temporarily unavailable.", noModels: "This account returned no available models.",
|
|
41
|
+
context: "Context", reasoning: "Reasoning", defaultEffort: "default", text: "Text input", streaming: "Streaming", tools: "Tool calling",
|
|
42
|
+
lastUpdated: "Updated",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const css = `
|
|
47
|
+
.dsh-grok-page{box-sizing:border-box;width:min(920px,100%);padding:8px 4px 40px;color:var(--dsw-alias-label-primary)}
|
|
48
|
+
.dsh-grok-page *{box-sizing:border-box}.dsh-grok-page h2{margin:0;font-size:21px;line-height:31px}.dsh-grok-description{max-width:760px;margin:5px 0 18px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:21px}
|
|
49
|
+
.dsh-grok-stack{display:grid;gap:14px}.dsh-grok-panel{overflow:hidden;border:1px solid rgb(190 93 138 / 28%);border-radius:18px;background:linear-gradient(145deg,rgb(190 93 138 / 8%),rgb(190 93 138 / 3%) 55%,var(--dsw-alias-bg-layer-1));box-shadow:0 1px 2px rgb(20 20 30 / 3%)}
|
|
50
|
+
.dsh-grok-panel-inner{padding:20px}.dsh-grok-account-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.dsh-grok-state{display:flex;min-width:0;align-items:center;gap:10px;font-size:17px;font-weight:650}.dsh-grok-dot{width:10px;height:10px;flex:0 0 auto;border-radius:50%;background:var(--dsw-alias-label-tertiary,#909090)}.dsh-grok-dot[data-good="true"]{background:var(--dsw-static-green-500,#1ebf68);box-shadow:0 0 0 4px rgb(30 191 104 / 10%)}.dsh-grok-dot[data-running="true"]{background:#d99b2b;box-shadow:0 0 0 4px rgb(217 155 43 / 12%)}
|
|
51
|
+
.dsh-grok-button{height:36px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 14px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);cursor:pointer;font:inherit;font-size:12px}.dsh-grok-button:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsh-grok-button:disabled{cursor:default;opacity:.45}.dsh-grok-primary{border-color:var(--dsw-static-deepseek-500,#4d6bfe);color:#fff;background:var(--dsw-static-deepseek-500,#4d6bfe)}
|
|
52
|
+
.dsh-grok-auth-note{margin:13px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:19px}.dsh-grok-actions{display:flex;flex-wrap:wrap;gap:7px}
|
|
53
|
+
.dsh-grok-divider{height:1px;margin:18px 0;background:rgb(190 93 138 / 20%)}.dsh-grok-section-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsh-grok-section-head h3{margin:0;font-size:15px;line-height:23px}.dsh-grok-section-head .dsh-grok-button{height:30px;padding:0 11px}
|
|
54
|
+
.dsh-grok-quota{margin-top:15px}.dsh-grok-quota-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;color:var(--dsw-alias-label-secondary);font-size:13px}.dsh-grok-quota-name{color:var(--dsw-alias-label-primary);font-weight:600}.dsh-grok-progress{height:9px;overflow:hidden;border-radius:999px;background:rgb(190 93 138 / 13%)}.dsh-grok-progress-fill{height:100%;border-radius:inherit;background:linear-gradient(90deg,#d7448a,#ea1768);transition:width .25s ease}.dsh-grok-quota-meta{display:flex;flex-wrap:wrap;justify-content:space-between;gap:7px;margin-top:9px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.dsh-grok-muted{margin:12px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:19px}
|
|
55
|
+
.dsh-grok-models-head{padding:20px 20px 0}.dsh-grok-models-head h3{margin:0;font-size:16px;line-height:24px}.dsh-grok-models-head p{margin:5px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:19px}.dsh-grok-model-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:16px 20px 20px}.dsh-grok-model{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:13px;padding:14px;background:var(--dsw-alias-bg-layer-1)}.dsh-grok-model-title{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsh-grok-model-title strong{overflow-wrap:anywhere;font-size:14px}.dsh-grok-model-id{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px}.dsh-grok-model-description{margin:5px 0 10px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}.dsh-grok-badges{display:flex;flex-wrap:wrap;gap:5px}.dsh-grok-badge{border-radius:999px;padding:2px 8px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);font-size:10px;line-height:18px}.dsh-grok-badge[data-accent="true"]{color:#a9326b;background:rgb(215 68 138 / 11%)}.dsh-grok-efforts{margin-top:10px;color:var(--dsw-alias-label-secondary);font-size:10px;line-height:17px}.dsh-grok-footer{padding:0 20px 16px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:10px}
|
|
56
|
+
.dsh-grok-notice{padding:13px 14px;border:1px solid rgb(229 72 77 / 28%);border-radius:11px;color:var(--dsw-alias-label-secondary);background:rgb(229 72 77 / 7%);font-size:12px;line-height:19px}.dsh-grok-error{margin:10px 0 0;color:var(--dsw-static-red-500,#e5484d);font-size:12px}
|
|
57
|
+
@media(max-width:680px){.dsh-grok-panel-inner,.dsh-grok-models-head{padding:16px}.dsh-grok-model-grid{grid-template-columns:1fr;padding:14px 16px 16px}.dsh-grok-account-head{align-items:flex-start;flex-direction:column}.dsh-grok-quota-row{align-items:flex-start;flex-direction:column;gap:3px}}
|
|
58
|
+
`
|
|
59
|
+
if (!document.querySelector('style[data-plugin-css="dsh-grok-provider"]')) {
|
|
60
|
+
const style = document.createElement("style")
|
|
61
|
+
style.dataset.pluginCss = "dsh-grok-provider"
|
|
62
|
+
style.textContent = css
|
|
63
|
+
document.head.appendChild(style)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function GrokSettings({ connection, t }) {
|
|
67
|
+
const [status, setStatus] = React.useState()
|
|
68
|
+
const [dashboard, setDashboard] = React.useState()
|
|
69
|
+
const [busy, setBusy] = React.useState(false)
|
|
70
|
+
const [error, setError] = React.useState(false)
|
|
71
|
+
const [dashboardError, setDashboardError] = React.useState(false)
|
|
72
|
+
const [confirmLogout, setConfirmLogout] = React.useState(false)
|
|
73
|
+
const call = React.useCallback(async (endpoint, payload = {}) => {
|
|
74
|
+
const result = await connection.rpc.call("/grok-auth", endpoint, payload)
|
|
75
|
+
if (!result || result.ok !== true) throw new Error("Grok account RPC failed")
|
|
76
|
+
return result.value
|
|
77
|
+
}, [connection])
|
|
78
|
+
const refreshDashboard = React.useCallback(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const value = await call("dashboard")
|
|
81
|
+
setDashboard(value.dashboard)
|
|
82
|
+
setDashboardError(false)
|
|
83
|
+
} catch { setDashboardError(true) }
|
|
84
|
+
}, [call])
|
|
85
|
+
const refreshStatus = React.useCallback(async ({ includeDashboard = false } = {}) => {
|
|
86
|
+
try {
|
|
87
|
+
const value = await call("status")
|
|
88
|
+
setStatus(value.status)
|
|
89
|
+
setError(false)
|
|
90
|
+
if (value.status.available === true && includeDashboard) await refreshDashboard()
|
|
91
|
+
if (value.status.available !== true) setDashboard(undefined)
|
|
92
|
+
} catch { setError(true) }
|
|
93
|
+
}, [call, refreshDashboard])
|
|
94
|
+
React.useEffect(() => { refreshStatus({ includeDashboard: true }) }, [refreshStatus])
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (status?.session?.state !== "running") return undefined
|
|
97
|
+
const timer = window.setInterval(() => refreshStatus(), 1_000)
|
|
98
|
+
return () => window.clearInterval(timer)
|
|
99
|
+
}, [refreshStatus, status])
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (status?.available === true && status?.session?.state === "succeeded") refreshDashboard()
|
|
102
|
+
}, [refreshDashboard, status?.available, status?.session?.state])
|
|
103
|
+
const act = async (operation) => {
|
|
104
|
+
setBusy(true); setError(false)
|
|
105
|
+
try { await operation() } catch { setError(true) } finally { setBusy(false) }
|
|
106
|
+
}
|
|
107
|
+
const login = () => act(async () => {
|
|
108
|
+
await call("login"); setConfirmLogout(false); await refreshStatus()
|
|
109
|
+
})
|
|
110
|
+
const cancel = () => act(async () => {
|
|
111
|
+
const sessionId = status?.session?.sessionId
|
|
112
|
+
if (typeof sessionId !== "string") throw new Error("No active Grok login")
|
|
113
|
+
await call("cancel", { sessionId }); await refreshStatus({ includeDashboard: true })
|
|
114
|
+
})
|
|
115
|
+
const logout = () => act(async () => {
|
|
116
|
+
const value = await call("logout")
|
|
117
|
+
if (value.kind === "logout-confirmation-required") setConfirmLogout(true)
|
|
118
|
+
else { setConfirmLogout(false); setDashboard(undefined); await refreshStatus() }
|
|
119
|
+
})
|
|
120
|
+
const manualRefresh = () => act(async () => refreshStatus({ includeDashboard: true }))
|
|
121
|
+
|
|
122
|
+
if (!connection.isLoopback) return React.createElement("section", { className: "dsh-grok-page" },
|
|
123
|
+
React.createElement("h2", null, t("title")), React.createElement("p", { className: "dsh-grok-notice" }, t("loopback")))
|
|
124
|
+
if (!status && !error) return React.createElement("section", { className: "dsh-grok-page" }, t("loading"))
|
|
125
|
+
|
|
126
|
+
const driver = status?.driver === true
|
|
127
|
+
const available = status?.available === true
|
|
128
|
+
const sessionState = status?.session?.state
|
|
129
|
+
const stateLabel = !driver ? t("driverMissing") : sessionState === "running" ? t("running")
|
|
130
|
+
: sessionState === "succeeded" && available ? t("succeeded") : sessionState === "failed" ? t("failed")
|
|
131
|
+
: sessionState === "cancelled" ? t("cancelled") : available ? t("ready") : t("notReady")
|
|
132
|
+
|
|
133
|
+
const renderQuota = () => {
|
|
134
|
+
if (!available) return React.createElement("p", { className: "dsh-grok-muted" }, t("notReady"))
|
|
135
|
+
if (!dashboard && !dashboardError) return React.createElement("p", { className: "dsh-grok-muted" }, t("refreshing"))
|
|
136
|
+
const quota = dashboard?.quota
|
|
137
|
+
if (dashboardError || quota?.state !== "ready") return React.createElement("p", { className: "dsh-grok-muted" }, t("quotaUnavailable"))
|
|
138
|
+
const hasPercent = typeof quota.remainingPercent === "number"
|
|
139
|
+
const periodLabel = quota.periodKind === "weekly" ? t("weekly") : quota.periodKind === "monthly" ? t("monthly") : t("currentPeriod")
|
|
140
|
+
return React.createElement("div", { className: "dsh-grok-quota" },
|
|
141
|
+
React.createElement("div", { className: "dsh-grok-quota-row" },
|
|
142
|
+
React.createElement("span", { className: "dsh-grok-quota-name" }, t("grokBuild")),
|
|
143
|
+
React.createElement("span", null, hasPercent ? `${t("remaining")} ${formatPercent(quota.remainingPercent)}` : t("usageUnknown"))),
|
|
144
|
+
hasPercent && React.createElement("div", { className: "dsh-grok-progress", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": quota.remainingPercent },
|
|
145
|
+
React.createElement("div", { className: "dsh-grok-progress-fill", style: { width: `${quota.remainingPercent}%` } })),
|
|
146
|
+
React.createElement("div", { className: "dsh-grok-quota-meta" },
|
|
147
|
+
React.createElement("span", null, hasPercent ? `${periodLabel} · ${t("used")} ${formatPercent(quota.usedPercent)}` : periodLabel),
|
|
148
|
+
React.createElement("span", null, quota.resetsAt ? `${t("resetTime")}:${formatDateTime(quota.resetsAt)}` : t("resetUnknown"))))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const renderModel = (model) => {
|
|
152
|
+
const effortText = model.reasoning?.efforts?.map((effort) => effort.id === model.reasoning.defaultEffort
|
|
153
|
+
? `${effort.name} (${t("defaultEffort")})` : effort.name).join(" · ")
|
|
154
|
+
return React.createElement("article", { className: "dsh-grok-model", key: model.id },
|
|
155
|
+
React.createElement("div", { className: "dsh-grok-model-title" },
|
|
156
|
+
React.createElement("strong", null, model.name), React.createElement("span", { className: "dsh-grok-model-id" }, model.id)),
|
|
157
|
+
model.description && React.createElement("p", { className: "dsh-grok-model-description" }, model.description),
|
|
158
|
+
React.createElement("div", { className: "dsh-grok-badges" },
|
|
159
|
+
React.createElement("span", { className: "dsh-grok-badge", "data-accent": true }, `${t("context")} ${formatContext(model.contextWindow)}`),
|
|
160
|
+
model.capabilities?.textInput && React.createElement("span", { className: "dsh-grok-badge" }, t("text")),
|
|
161
|
+
model.capabilities?.streaming && React.createElement("span", { className: "dsh-grok-badge" }, t("streaming")),
|
|
162
|
+
model.capabilities?.functionTools && React.createElement("span", { className: "dsh-grok-badge" }, t("tools"))),
|
|
163
|
+
effortText && React.createElement("div", { className: "dsh-grok-efforts" }, `${t("reasoning")}:${effortText}`))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const models = dashboard?.models
|
|
167
|
+
return React.createElement("section", { className: "dsh-grok-page" },
|
|
168
|
+
React.createElement("h2", null, t("title")), React.createElement("p", { className: "dsh-grok-description" }, t("description")),
|
|
169
|
+
React.createElement("div", { className: "dsh-grok-stack" },
|
|
170
|
+
React.createElement("article", { className: "dsh-grok-panel" }, React.createElement("div", { className: "dsh-grok-panel-inner" },
|
|
171
|
+
React.createElement("div", { className: "dsh-grok-account-head" },
|
|
172
|
+
React.createElement("div", { className: "dsh-grok-state" }, React.createElement("span", { className: "dsh-grok-dot", "data-good": available, "data-running": sessionState === "running" }), stateLabel),
|
|
173
|
+
React.createElement("div", { className: "dsh-grok-actions" },
|
|
174
|
+
sessionState === "running"
|
|
175
|
+
? React.createElement("button", { className: "dsh-grok-button", type: "button", disabled: busy, onClick: cancel }, t("cancel"))
|
|
176
|
+
: !available && React.createElement("button", { className: "dsh-grok-button dsh-grok-primary", type: "button", disabled: busy || !driver, onClick: login }, t("login")),
|
|
177
|
+
available && React.createElement("button", { className: "dsh-grok-button", type: "button", disabled: busy || !driver, onClick: logout }, t(confirmLogout ? "confirmLogout" : "logout")))),
|
|
178
|
+
React.createElement("p", { className: "dsh-grok-auth-note" }, t("officialBody")),
|
|
179
|
+
React.createElement("div", { className: "dsh-grok-divider" }),
|
|
180
|
+
React.createElement("div", { className: "dsh-grok-section-head" }, React.createElement("h3", null, t("usage")),
|
|
181
|
+
React.createElement("button", { className: "dsh-grok-button", type: "button", disabled: busy || !available, onClick: manualRefresh }, busy ? t("refreshing") : t("refresh"))),
|
|
182
|
+
renderQuota(), error && React.createElement("p", { className: "dsh-grok-error" }, t("unavailable")))),
|
|
183
|
+
React.createElement("article", { className: "dsh-grok-panel" },
|
|
184
|
+
React.createElement("div", { className: "dsh-grok-models-head" }, React.createElement("h3", null, t("modelsTitle")), React.createElement("p", null, t("modelsDescription"))),
|
|
185
|
+
React.createElement("div", { className: "dsh-grok-model-grid" },
|
|
186
|
+
!available ? React.createElement("p", { className: "dsh-grok-muted" }, t("notReady"))
|
|
187
|
+
: dashboardError || models?.state === "unavailable" ? React.createElement("p", { className: "dsh-grok-muted" }, t("modelsUnavailable"))
|
|
188
|
+
: !models ? React.createElement("p", { className: "dsh-grok-muted" }, t("refreshing"))
|
|
189
|
+
: models.items.length === 0 ? React.createElement("p", { className: "dsh-grok-muted" }, t("noModels"))
|
|
190
|
+
: models.items.map(renderModel)),
|
|
191
|
+
dashboard?.fetchedAt && React.createElement("div", { className: "dsh-grok-footer" }, `${t("lastUpdated")}:${formatDateTime(dashboard.fetchedAt)}`))))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatPercent(value) { return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value)}%` }
|
|
195
|
+
function formatDateTime(value) { return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)) }
|
|
196
|
+
function formatContext(value) {
|
|
197
|
+
if (value >= 1_000_000) return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(value / 1_000_000)}M`
|
|
198
|
+
if (value >= 1_000) return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value / 1_000)}K`
|
|
199
|
+
return new Intl.NumberFormat().format(value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const inject = ["slots", "locale", "connection"]
|
|
203
|
+
function apply(ctx) {
|
|
204
|
+
ctx.effect(() => ctx.locale.register(namespace, dictionaries), "dsh-grok: dictionaries")
|
|
205
|
+
const t = ctx.locale.bind(namespace)
|
|
206
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
207
|
+
name: "settings.section", id: "grok-auth", order: 45, label: () => t("nav"), locale: namespace,
|
|
208
|
+
inject: () => ({ connection: ctx.connection, t }),
|
|
209
|
+
}, GrokSettings))
|
|
210
|
+
}
|
|
211
|
+
module.exports.inject = inject
|
|
212
|
+
module.exports.apply = apply
|
|
213
|
+
return module.exports
|
|
214
|
+
},
|
|
215
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { randomUUID } from "node:crypto"
|
|
4
|
+
|
|
5
|
+
import { LlmError, attributionHeaders } from "@deepseek-ai/dsh-llm"
|
|
6
|
+
import Schema from "@deepseek-ai/schemastery"
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CredentialFileTooLargeError,
|
|
10
|
+
GROK_CLI_1_0_5_AUTH_CONTRACT,
|
|
11
|
+
UnsupportedCredentialError,
|
|
12
|
+
createCredentialSource,
|
|
13
|
+
} from "../internal/credential-source.mjs"
|
|
14
|
+
import { AuthModeUnavailableError } from "../internal/auth-registry.mjs"
|
|
15
|
+
import { createAccountDashboard } from "../internal/account-dashboard.mjs"
|
|
16
|
+
import { createAuthController } from "../internal/auth-controller.mjs"
|
|
17
|
+
import { createAuthRpcHandler } from "../internal/auth-rpc.mjs"
|
|
18
|
+
import { createGrokAdapter } from "../internal/grok-adapter.mjs"
|
|
19
|
+
import { createGrokCommandHandler } from "../internal/grok-command-handler.mjs"
|
|
20
|
+
import { GrokTransportError, createGrokTransport } from "../internal/grok-transport.mjs"
|
|
21
|
+
import {
|
|
22
|
+
OfficialCredentialFileError,
|
|
23
|
+
createOfficialCredentialLoader,
|
|
24
|
+
} from "../internal/official-credential-loader.mjs"
|
|
25
|
+
import { createOfficialCliAuth } from "../internal/official-cli-auth.mjs"
|
|
26
|
+
import { createOfficialAuthDriver } from "../internal/official-auth-driver.mjs"
|
|
27
|
+
import { verifyOfficialCliExecutable } from "../internal/official-cli-verifier.mjs"
|
|
28
|
+
import { installProviderRuntime } from "../internal/provider-runtime.mjs"
|
|
29
|
+
|
|
30
|
+
export const name = "llm-grok"
|
|
31
|
+
export const inject = ["llm"]
|
|
32
|
+
|
|
33
|
+
export const Config = Schema.object({})
|
|
34
|
+
|
|
35
|
+
export function apply(ctx) {
|
|
36
|
+
const platform = process.platform
|
|
37
|
+
if (platform !== "darwin" && platform !== "win32") {
|
|
38
|
+
throw new TypeError("dsh-grok-provider supports macOS and Windows in version 0.1.0")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const homeDir = os.homedir()
|
|
42
|
+
let refreshOfficialCredential
|
|
43
|
+
const officialSource = createCredentialSource({
|
|
44
|
+
contract: GROK_CLI_1_0_5_AUTH_CONTRACT,
|
|
45
|
+
load: createOfficialCredentialLoader({
|
|
46
|
+
authPath: path.join(homeDir, ".grok", "auth.json"),
|
|
47
|
+
platform,
|
|
48
|
+
}),
|
|
49
|
+
now: () => new Date(),
|
|
50
|
+
refresh: async () => {
|
|
51
|
+
if (refreshOfficialCredential === undefined) throw new UnsupportedCredentialError()
|
|
52
|
+
await refreshOfficialCredential()
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
const runtime = installProviderRuntime({
|
|
56
|
+
llm: ctx.llm,
|
|
57
|
+
officialSource,
|
|
58
|
+
createTransport: (credentialSource) => createGrokTransport({
|
|
59
|
+
credentialSource,
|
|
60
|
+
fetch: globalThis.fetch,
|
|
61
|
+
attributionHeaders,
|
|
62
|
+
clientIdentifier: "dsh-grok-provider",
|
|
63
|
+
clientVersion: "1.0.5",
|
|
64
|
+
}),
|
|
65
|
+
createAdapter: ({ getGeneration }) => createGrokAdapter({
|
|
66
|
+
getGeneration,
|
|
67
|
+
mapError: mapLlmError,
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
const authController = createAuthController({
|
|
71
|
+
registry: runtime.auth,
|
|
72
|
+
randomUUID,
|
|
73
|
+
})
|
|
74
|
+
const commandHandler = createGrokCommandHandler({ controller: authController })
|
|
75
|
+
const dashboard = createAccountDashboard({
|
|
76
|
+
listModels: () => runtime.adapter.listModels("grok"),
|
|
77
|
+
getBilling: ({ signal } = {}) => runtime.auth.getGeneration().transport.getBilling({ signal }),
|
|
78
|
+
now: () => new Date(),
|
|
79
|
+
})
|
|
80
|
+
const authRpcHandler = createAuthRpcHandler({ controller: authController, dashboard })
|
|
81
|
+
|
|
82
|
+
ctx.inject(["subprocess"], (subprocessCtx) => {
|
|
83
|
+
const officialAuth = createOfficialCliAuth({
|
|
84
|
+
subprocess: subprocessCtx.subprocess,
|
|
85
|
+
platform,
|
|
86
|
+
homeDir,
|
|
87
|
+
verifyExecutable: verifyOfficialCliExecutable,
|
|
88
|
+
})
|
|
89
|
+
const refresh = async () => {
|
|
90
|
+
const outcome = await officialAuth.refresh()
|
|
91
|
+
if (outcome.kind !== "succeeded") throw new UnsupportedCredentialError()
|
|
92
|
+
}
|
|
93
|
+
refreshOfficialCredential = refresh
|
|
94
|
+
const removeDriver = authController.installDriver(createOfficialAuthDriver({
|
|
95
|
+
officialAuth,
|
|
96
|
+
credentialSource: officialSource,
|
|
97
|
+
}))
|
|
98
|
+
return async () => {
|
|
99
|
+
await authController.shutdown()
|
|
100
|
+
if (refreshOfficialCredential === refresh) refreshOfficialCredential = undefined
|
|
101
|
+
removeDriver()
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
ctx.inject(["commands"], (commandsCtx) => commandsCtx.commands.register({
|
|
106
|
+
name: "grok",
|
|
107
|
+
description: "Manage Grok Build authentication",
|
|
108
|
+
input: { hint: "status|login|cancel|logout" },
|
|
109
|
+
recordInput: false,
|
|
110
|
+
handler: commandHandler,
|
|
111
|
+
}))
|
|
112
|
+
ctx.inject(["connection"], (connectionCtx) => connectionCtx.connection.rpc.handle(
|
|
113
|
+
"/grok-auth",
|
|
114
|
+
authRpcHandler,
|
|
115
|
+
{ authority: "loopback" },
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
ctx.effect(() => () => runtime.dispose(), "llm-grok runtime")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function mapLlmError(error) {
|
|
122
|
+
if (error instanceof LlmError) return error
|
|
123
|
+
if (error?.name === "AbortError") {
|
|
124
|
+
return new LlmError("The Grok Build request was cancelled", "ABORTED", { cause: error })
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
error instanceof AuthModeUnavailableError ||
|
|
128
|
+
error instanceof UnsupportedCredentialError ||
|
|
129
|
+
error instanceof CredentialFileTooLargeError ||
|
|
130
|
+
error instanceof OfficialCredentialFileError ||
|
|
131
|
+
(error instanceof GrokTransportError && (error.status === 401 || error.status === 403))
|
|
132
|
+
) {
|
|
133
|
+
return new LlmError("Grok authentication is required", "AUTH", {
|
|
134
|
+
cause: error,
|
|
135
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
if (error instanceof GrokTransportError) {
|
|
139
|
+
return new LlmError("The Grok Build request failed", error.status === 429 ? "RATE_LIMIT" : "PROVIDER_ERROR", {
|
|
140
|
+
cause: error,
|
|
141
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
return new LlmError("The Grok provider rejected an invalid or unsupported response", "INVALID_RESPONSE", {
|
|
145
|
+
cause: error,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseBillingSummary } from "./billing-summary.mjs"
|
|
2
|
+
|
|
3
|
+
export function createAccountDashboard({ listModels, getBilling, now }) {
|
|
4
|
+
if (typeof listModels !== "function" || typeof getBilling !== "function" || typeof now !== "function") {
|
|
5
|
+
throw new TypeError("Invalid Grok account dashboard dependencies")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return async function getAccountDashboard({ signal } = {}) {
|
|
9
|
+
const [modelResult, quotaResult] = await Promise.allSettled([
|
|
10
|
+
Promise.resolve().then(() => listModels({ signal })).then((models) => models.map(projectModel)),
|
|
11
|
+
Promise.resolve().then(() => getBilling({ signal })).then(parseBillingSummary),
|
|
12
|
+
])
|
|
13
|
+
const currentTime = now()
|
|
14
|
+
if (!(currentTime instanceof Date) || !Number.isFinite(currentTime.getTime())) {
|
|
15
|
+
throw new TypeError("Invalid Grok account dashboard clock")
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
fetchedAt: currentTime.toISOString(),
|
|
19
|
+
models: modelResult.status === "fulfilled"
|
|
20
|
+
? Object.freeze({ state: "ready", items: Object.freeze(modelResult.value) })
|
|
21
|
+
: Object.freeze({ state: "unavailable", items: Object.freeze([]) }),
|
|
22
|
+
quota: quotaResult.status === "fulfilled"
|
|
23
|
+
? quotaResult.value
|
|
24
|
+
: Object.freeze({ state: "unavailable" }),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function projectModel(model) {
|
|
30
|
+
if (
|
|
31
|
+
!isPlainObject(model) ||
|
|
32
|
+
typeof model.id !== "string" ||
|
|
33
|
+
typeof model.name !== "string" ||
|
|
34
|
+
!isPlainObject(model.context) ||
|
|
35
|
+
!Number.isSafeInteger(model.context.contextWindow)
|
|
36
|
+
) throw new TypeError("Invalid Grok dashboard model")
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
id: model.id,
|
|
39
|
+
name: model.name,
|
|
40
|
+
...(typeof model.description === "string" ? { description: model.description } : {}),
|
|
41
|
+
contextWindow: model.context.contextWindow,
|
|
42
|
+
...(model.reasoning === undefined ? {} : { reasoning: projectReasoning(model.reasoning) }),
|
|
43
|
+
capabilities: Object.freeze({ textInput: true, streaming: true, functionTools: true }),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function projectReasoning(reasoning) {
|
|
48
|
+
if (!isPlainObject(reasoning) || !Array.isArray(reasoning.efforts)) {
|
|
49
|
+
throw new TypeError("Invalid Grok dashboard reasoning capability")
|
|
50
|
+
}
|
|
51
|
+
const efforts = reasoning.efforts.map((effort) => {
|
|
52
|
+
if (!isPlainObject(effort) || typeof effort.id !== "string" || typeof effort.name !== "string") {
|
|
53
|
+
throw new TypeError("Invalid Grok dashboard reasoning effort")
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({ id: effort.id, name: effort.name })
|
|
56
|
+
})
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
efforts: Object.freeze(efforts),
|
|
59
|
+
...(typeof reasoning.defaultEffort === "string" ? { defaultEffort: reasoning.defaultEffort } : {}),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isPlainObject(value) {
|
|
64
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
65
|
+
const prototype = Object.getPrototypeOf(value)
|
|
66
|
+
return prototype === Object.prototype || prototype === null
|
|
67
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const OUTCOMES = new Set(["succeeded", "cancelled", "failed"])
|
|
2
|
+
|
|
3
|
+
export class AuthLoginBusyError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("A Grok login or logout is already running")
|
|
6
|
+
this.name = "AuthLoginBusyError"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class AuthDriverUnavailableError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("The official Grok authentication driver is unavailable")
|
|
13
|
+
this.name = "AuthDriverUnavailableError"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createAuthController({ registry, randomUUID, now = () => new Date(), driver }) {
|
|
18
|
+
if (
|
|
19
|
+
!registry ||
|
|
20
|
+
typeof registry.invalidate !== "function" ||
|
|
21
|
+
typeof registry.status !== "function" ||
|
|
22
|
+
typeof randomUUID !== "function" ||
|
|
23
|
+
typeof now !== "function"
|
|
24
|
+
) throw new TypeError("Invalid Grok auth controller dependencies")
|
|
25
|
+
|
|
26
|
+
let installed
|
|
27
|
+
let active
|
|
28
|
+
let state
|
|
29
|
+
let confirmation
|
|
30
|
+
let closing = false
|
|
31
|
+
if (driver !== undefined) install(driver)
|
|
32
|
+
|
|
33
|
+
function install(nextDriver) {
|
|
34
|
+
if (!nextDriver || typeof nextDriver.begin !== "function") {
|
|
35
|
+
throw new TypeError("Invalid Grok auth driver")
|
|
36
|
+
}
|
|
37
|
+
const token = Object.freeze({})
|
|
38
|
+
installed = { driver: nextDriver, token }
|
|
39
|
+
return () => {
|
|
40
|
+
if (installed?.token === token) installed = undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
installDriver: install,
|
|
46
|
+
|
|
47
|
+
async status() {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
...await registry.status(),
|
|
50
|
+
driver: installed !== undefined,
|
|
51
|
+
...(state === undefined ? {} : { session: Object.freeze({ ...state }) }),
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async beginLogin() {
|
|
56
|
+
if (active !== undefined || closing) throw new AuthLoginBusyError()
|
|
57
|
+
if (installed === undefined) throw new AuthDriverUnavailableError()
|
|
58
|
+
|
|
59
|
+
const sessionId = createId(randomUUID, "Invalid Grok auth session id")
|
|
60
|
+
const abortController = new AbortController()
|
|
61
|
+
let started
|
|
62
|
+
try {
|
|
63
|
+
started = await installed.driver.begin({ signal: abortController.signal })
|
|
64
|
+
} catch {
|
|
65
|
+
state = { state: "failed", sessionId }
|
|
66
|
+
registry.invalidate()
|
|
67
|
+
throw new AuthDriverUnavailableError()
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
!isPlainObject(started) ||
|
|
71
|
+
Object.keys(started).length !== 1 ||
|
|
72
|
+
!started.completion ||
|
|
73
|
+
typeof started.completion.then !== "function"
|
|
74
|
+
) throw new TypeError("Invalid Grok auth driver session")
|
|
75
|
+
|
|
76
|
+
const publicState = Object.freeze({ sessionId, state: "running" })
|
|
77
|
+
state = publicState
|
|
78
|
+
const settled = Promise.resolve(started.completion).then(
|
|
79
|
+
(outcome) => normalizeOutcome(outcome),
|
|
80
|
+
() => Object.freeze({ kind: abortController.signal.aborted ? "cancelled" : "failed" }),
|
|
81
|
+
).then((outcome) => {
|
|
82
|
+
state = { state: outcome.kind, sessionId }
|
|
83
|
+
active = undefined
|
|
84
|
+
registry.invalidate()
|
|
85
|
+
return outcome
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const session = Object.freeze({
|
|
89
|
+
public: publicState,
|
|
90
|
+
cancel() {
|
|
91
|
+
abortController.abort()
|
|
92
|
+
},
|
|
93
|
+
async wait({ signal } = {}) {
|
|
94
|
+
if (!signal) return settled
|
|
95
|
+
if (signal.aborted) {
|
|
96
|
+
abortController.abort(signal.reason)
|
|
97
|
+
return settled
|
|
98
|
+
}
|
|
99
|
+
const abort = () => abortController.abort(signal.reason)
|
|
100
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
101
|
+
try {
|
|
102
|
+
return await settled
|
|
103
|
+
} finally {
|
|
104
|
+
signal.removeEventListener("abort", abort)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
active = session
|
|
109
|
+
return session
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
cancel(sessionId) {
|
|
113
|
+
if (active === undefined || active.public.sessionId !== sessionId) return false
|
|
114
|
+
active.cancel()
|
|
115
|
+
return true
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async shutdown() {
|
|
119
|
+
if (active === undefined) return false
|
|
120
|
+
active.cancel()
|
|
121
|
+
await active.wait()
|
|
122
|
+
return true
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async logout({ signal } = {}) {
|
|
126
|
+
if (closing) throw new AuthLoginBusyError()
|
|
127
|
+
if (installed === undefined || typeof installed.driver.logout !== "function") {
|
|
128
|
+
throw new AuthDriverUnavailableError()
|
|
129
|
+
}
|
|
130
|
+
const currentTime = now()
|
|
131
|
+
if (!(currentTime instanceof Date) || !Number.isFinite(currentTime.getTime())) {
|
|
132
|
+
throw new TypeError("Invalid Grok auth controller clock")
|
|
133
|
+
}
|
|
134
|
+
if (confirmation === undefined || confirmation.expiresAt <= currentTime.getTime()) {
|
|
135
|
+
const confirmationId = createId(randomUUID, "Invalid Grok logout confirmation id")
|
|
136
|
+
const expiresAt = currentTime.getTime() + 30_000
|
|
137
|
+
confirmation = { confirmationId, expiresAt }
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
kind: "confirmation-required",
|
|
140
|
+
confirmationId,
|
|
141
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
confirmation = undefined
|
|
146
|
+
closing = true
|
|
147
|
+
try {
|
|
148
|
+
if (active !== undefined) {
|
|
149
|
+
active.cancel()
|
|
150
|
+
await active.wait()
|
|
151
|
+
}
|
|
152
|
+
let outcome
|
|
153
|
+
try {
|
|
154
|
+
outcome = normalizeOutcome(await installed.driver.logout({ signal }))
|
|
155
|
+
} catch {
|
|
156
|
+
outcome = Object.freeze({ kind: signal?.aborted ? "cancelled" : "failed" })
|
|
157
|
+
}
|
|
158
|
+
registry.invalidate()
|
|
159
|
+
return outcome
|
|
160
|
+
} finally {
|
|
161
|
+
closing = false
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeOutcome(outcome) {
|
|
168
|
+
if (!isPlainObject(outcome) || !OUTCOMES.has(outcome.kind) || Object.keys(outcome).length !== 1) {
|
|
169
|
+
return Object.freeze({ kind: "failed" })
|
|
170
|
+
}
|
|
171
|
+
return Object.freeze({ kind: outcome.kind })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createId(randomUUID, message) {
|
|
175
|
+
const value = randomUUID()
|
|
176
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128) throw new TypeError(message)
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isPlainObject(value) {
|
|
181
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
182
|
+
const prototype = Object.getPrototypeOf(value)
|
|
183
|
+
return prototype === Object.prototype || prototype === null
|
|
184
|
+
}
|