@elliotsystems/eai 0.1.4 → 0.1.7
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/README.md +84 -4
- package/bin/eai +139 -2
- package/lib/connect.js +205 -0
- package/lib/gdrive.js +238 -0
- package/lib/logout.js +73 -0
- package/lib/setup.js +534 -24
- package/lib/stage.js +100 -6
- package/lib/status.js +133 -0
- package/lib/token-env.js +53 -17
- package/lib/update-check.js +27 -0
- package/lib/update.js +303 -0
- package/package.json +3 -2
- package/plugin/opencode-plugin/package.json +1 -1
- package/plugin/opencode-plugin/src/classifier.ts +2 -1
- package/plugin/opencode-plugin/src/elliot-auth.ts +56 -104
- package/plugin/opencode-plugin/src/identity-headers.ts +81 -0
- package/plugin/opencode-plugin/src/index.ts +22 -31
- package/plugin/opencode-plugin/src/mcp-keys.ts +132 -1
- package/plugin/opencode-plugin/src/policy.ts +36 -4
- package/plugin/opencode-plugin/src/proxy-provider.ts +153 -72
- package/plugin/opencode-plugin/src/scope.ts +2 -1
- package/plugin/opencode-plugin/src/tui/format.ts +37 -0
- package/plugin/opencode-plugin/src/tui/knowledge.ts +9 -3
- package/plugin/opencode-plugin/src/tui/sidebar.tsx +90 -0
- package/plugin/opencode-plugin/src/tui/usage.ts +132 -0
package/lib/stage.js
CHANGED
|
@@ -24,10 +24,40 @@
|
|
|
24
24
|
* upstream's own chrome.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
const crypto = require("crypto")
|
|
27
28
|
const fs = require("fs")
|
|
28
29
|
const os = require("os")
|
|
29
30
|
const path = require("path")
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The LiteLLM MCP gateway's alias, from `~/.elliot/config.json`'s
|
|
34
|
+
* `gateway_alias` field -- `eai setup` writes it there straight out of
|
|
35
|
+
* `/onboarding/config/{tenant_id}`'s own `gateway_alias`, which is
|
|
36
|
+
* `litellm_mcp_server_alias` (default `"elliot"`) read server-side.
|
|
37
|
+
*
|
|
38
|
+
* REQUIRED by the plugin's own options schema
|
|
39
|
+
* (`opencode-plugin/src/policy.ts`), with no default there on purpose: it
|
|
40
|
+
* used to be a hardcoded literal in the plugin itself, a fourth copy of a
|
|
41
|
+
* value that already lived in the backend settings, the backend's own
|
|
42
|
+
* namespace module and the LiteLLM YAML -- and it went stale exactly once,
|
|
43
|
+
* silently. This file must not reintroduce that by guessing `"elliot"` when
|
|
44
|
+
* the config is missing it; `undefined` here means "do not configure the
|
|
45
|
+
* plugin with a value nobody actually set", not "assume the default".
|
|
46
|
+
*
|
|
47
|
+
* Best-effort, like `token-env.js::injectToken` right next to it in the same
|
|
48
|
+
* launch path: a missing, unreadable or pre-this-field config must not block
|
|
49
|
+
* startup over branding.
|
|
50
|
+
*/
|
|
51
|
+
function readGatewayAlias() {
|
|
52
|
+
try {
|
|
53
|
+
const configPath = path.join(os.homedir(), ".elliot", "config.json")
|
|
54
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
55
|
+
return typeof config.gateway_alias === "string" && config.gateway_alias ? config.gateway_alias : undefined
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
/**
|
|
32
62
|
* Where the staged overlay lives.
|
|
33
63
|
*
|
|
@@ -43,6 +73,43 @@ function stageRoot(version, env = process.env) {
|
|
|
43
73
|
return path.join(base, version)
|
|
44
74
|
}
|
|
45
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A cheap content fingerprint of the overlay source.
|
|
78
|
+
*
|
|
79
|
+
* Staging used to be considered done whenever `.staged` held the current
|
|
80
|
+
* version, on the assumption that a version identifies its contents. That is
|
|
81
|
+
* false twice over: a package can be reinstalled with different contents under
|
|
82
|
+
* the same version (a pre-release build of 0.1.4 overwritten by the published
|
|
83
|
+
* 0.1.4), and a developer editing the plugin in place never changes the version
|
|
84
|
+
* at all. Both leave a stale tree staged, and the runtime then loads an OLD
|
|
85
|
+
* plugin out of a directory named after the NEW version -- with nothing logged,
|
|
86
|
+
* because from the runtime's point of view the plugin loaded fine. That is how
|
|
87
|
+
* a shipped fix (the agent calling itself Elliot rather than opencode) reached
|
|
88
|
+
* an installed machine and still did not take effect.
|
|
89
|
+
*
|
|
90
|
+
* Path + size + mtime, not file contents: this runs on every launch, and
|
|
91
|
+
* reading every file would put the cost back into startup that the marker
|
|
92
|
+
* exists to avoid. Anything npm or a build writes changes at least one mtime.
|
|
93
|
+
*/
|
|
94
|
+
function fingerprint(dir) {
|
|
95
|
+
const hash = crypto.createHash("sha256")
|
|
96
|
+
const walk = (current, prefix) => {
|
|
97
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
98
|
+
if (entry.name === "node_modules") continue
|
|
99
|
+
const abs = path.join(current, entry.name)
|
|
100
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
walk(abs, rel)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const stat = fs.statSync(abs)
|
|
106
|
+
hash.update(`${rel}\0${stat.size}\0${Math.trunc(stat.mtimeMs)}\n`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
walk(dir, "")
|
|
110
|
+
return hash.digest("hex").slice(0, 16)
|
|
111
|
+
}
|
|
112
|
+
|
|
46
113
|
function copyDir(from, to) {
|
|
47
114
|
fs.mkdirSync(to, { recursive: true })
|
|
48
115
|
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
@@ -66,6 +133,12 @@ function stageOverlay(opts) {
|
|
|
66
133
|
const packageRoot = opts.packageRoot
|
|
67
134
|
const version = opts.version
|
|
68
135
|
const env = opts.env || process.env
|
|
136
|
+
// Resolved by the CALLER's choice of source (bin/eai reads it from
|
|
137
|
+
// ~/.elliot/config.json; tests pass one directly) rather than read again in
|
|
138
|
+
// here, so there is exactly one place that decides where this value comes
|
|
139
|
+
// from. See readGatewayAlias's own docstring for why `undefined` must not
|
|
140
|
+
// become a guessed `"elliot"`.
|
|
141
|
+
const gatewayAlias = opts.gatewayAlias
|
|
69
142
|
|
|
70
143
|
const pluginSource = path.join(packageRoot, "plugin", "opencode-plugin")
|
|
71
144
|
const themeSource = path.join(packageRoot, "config", "themes")
|
|
@@ -76,15 +149,36 @@ function stageOverlay(opts) {
|
|
|
76
149
|
const pluginDir = path.join(root, "plugin", "opencode-plugin")
|
|
77
150
|
|
|
78
151
|
try {
|
|
79
|
-
// `.staged` records the version
|
|
80
|
-
//
|
|
81
|
-
|
|
152
|
+
// `.staged` records the version, a content fingerprint of the source this
|
|
153
|
+
// tree was built from, AND the gateway alias baked into the written
|
|
154
|
+
// config -- re-staging on every launch would add hundreds of file copies
|
|
155
|
+
// to startup, but skipping it on an alias change would leave a machine
|
|
156
|
+
// running `eai setup` against a renamed gateway alias (or configuring it
|
|
157
|
+
// for the first time) stuck on the stale tui.json/opencode.json until
|
|
158
|
+
// something else invalidated the stamp.
|
|
159
|
+
const stamp = `${version} ${fingerprint(pluginSource)} ${gatewayAlias || ""}`.trim()
|
|
160
|
+
const done = fs.existsSync(marker) && fs.readFileSync(marker, "utf8").trim() === stamp
|
|
82
161
|
if (!done) {
|
|
83
162
|
fs.rmSync(root, { recursive: true, force: true })
|
|
84
163
|
copyDir(pluginSource, pluginDir)
|
|
85
164
|
if (fs.existsSync(themeSource)) copyDir(themeSource, path.join(root, "themes"))
|
|
86
165
|
|
|
87
|
-
|
|
166
|
+
// The plugin's own OptionsSchema makes `gatewayAlias` REQUIRED with no
|
|
167
|
+
// default (`opencode-plugin/src/policy.ts`) -- a plugin spec with no
|
|
168
|
+
// options tuple here, the bare-path form `[pluginDir]`, fails that
|
|
169
|
+
// validation on every launch. That was this file's actual behaviour
|
|
170
|
+
// until the incident this comment documents: every bit of TUI branding
|
|
171
|
+
// the plugin provides (the "Elliot AI" footer, the knowledge panel)
|
|
172
|
+
// silently never rendered, logged but never surfaced.
|
|
173
|
+
//
|
|
174
|
+
// With no alias available (a config from before this field existed, or
|
|
175
|
+
// `eai setup` never run against an updated backend), the plugin is left
|
|
176
|
+
// OUT of the array entirely rather than included in a form guaranteed
|
|
177
|
+
// to fail — "plain OpenCode chrome" is the same visible outcome either
|
|
178
|
+
// way, but without logging a load error on every single launch.
|
|
179
|
+
const plugin = gatewayAlias
|
|
180
|
+
? [[pluginDir, { serverNames: ["elliot"], gatewayAlias }]]
|
|
181
|
+
: []
|
|
88
182
|
fs.writeFileSync(
|
|
89
183
|
path.join(root, "tui.json"),
|
|
90
184
|
JSON.stringify(
|
|
@@ -103,7 +197,7 @@ function stageOverlay(opts) {
|
|
|
103
197
|
path.join(root, "opencode.json"),
|
|
104
198
|
JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin }, null, 2),
|
|
105
199
|
)
|
|
106
|
-
fs.writeFileSync(marker,
|
|
200
|
+
fs.writeFileSync(marker, stamp)
|
|
107
201
|
}
|
|
108
202
|
return root
|
|
109
203
|
} catch {
|
|
@@ -113,4 +207,4 @@ function stageOverlay(opts) {
|
|
|
113
207
|
}
|
|
114
208
|
}
|
|
115
209
|
|
|
116
|
-
module.exports = { stageOverlay, stageRoot }
|
|
210
|
+
module.exports = { stageOverlay, stageRoot, readGatewayAlias }
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `eai status` — is this machine configured, can it reach the backend, and
|
|
5
|
+
* which connectors are live.
|
|
6
|
+
*
|
|
7
|
+
* Migrated from `elliot-ai status` as that CLI is retired. It is the one
|
|
8
|
+
* question `eai` could not answer about itself: the launcher knows where its
|
|
9
|
+
* config is, but a user staring at a failing session had no way to tell a
|
|
10
|
+
* missing token from an unreachable backend from a connector that was never
|
|
11
|
+
* authorised.
|
|
12
|
+
*
|
|
13
|
+
* Three facts, in the order they can fail:
|
|
14
|
+
*
|
|
15
|
+
* 1. config present — nothing else is meaningful without it
|
|
16
|
+
* 2. backend reachable — /health, so a network or DNS fault is named as one
|
|
17
|
+
* 3. connectors — /connectors/<tenant>, the thing people actually ask
|
|
18
|
+
*
|
|
19
|
+
* Each step degrades rather than aborting: an unreachable backend still prints
|
|
20
|
+
* the workspace block, because "which workspace am I in" is answerable offline
|
|
21
|
+
* and is half of what the command is for.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require("fs")
|
|
25
|
+
|
|
26
|
+
const { elliotConfigPath } = require("./setup.js")
|
|
27
|
+
|
|
28
|
+
/** Read ~/.elliot/config.json, or null when absent/unreadable. */
|
|
29
|
+
function readElliotConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(elliotConfigPath(), "utf8"))
|
|
32
|
+
return parsed && typeof parsed === "object" ? parsed : null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `/health`, with a timeout.
|
|
40
|
+
*
|
|
41
|
+
* Without one this hangs on a black-holed connection for the OS default, and a
|
|
42
|
+
* status command that hangs is worse than one that says "offline" — the user is
|
|
43
|
+
* running it precisely because something is already wrong.
|
|
44
|
+
*/
|
|
45
|
+
async function checkBackendHealth(backendUrl, timeoutMs = 5000) {
|
|
46
|
+
const controller = new AbortController()
|
|
47
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${String(backendUrl).replace(/\/+$/, "")}/health`, {
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
})
|
|
52
|
+
return res.ok
|
|
53
|
+
} catch {
|
|
54
|
+
return false
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timer)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Connector rows for this tenant, or [] when they cannot be fetched. */
|
|
61
|
+
async function fetchConnectors(config, timeoutMs = 8000) {
|
|
62
|
+
const controller = new AbortController()
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
64
|
+
try {
|
|
65
|
+
const base = String(config.backend_url || "").replace(/\/+$/, "")
|
|
66
|
+
const res = await fetch(`${base}/connectors/${config.tenant_id}`, {
|
|
67
|
+
headers: { Authorization: `Bearer ${config.jwt_token}` },
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
})
|
|
70
|
+
if (!res.ok) return []
|
|
71
|
+
const body = await res.json()
|
|
72
|
+
if (Array.isArray(body)) return body
|
|
73
|
+
return Array.isArray(body && body.connectors) ? body.connectors : []
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Title-case a provider id for display, without assuming it is non-empty. */
|
|
82
|
+
function displayName(provider) {
|
|
83
|
+
const name = String(provider || "unknown")
|
|
84
|
+
return name.charAt(0).toUpperCase() + name.slice(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {{log?: (s: string) => void, config?: object|null}} [opts]
|
|
89
|
+
* @returns {Promise<number>} the process exit code
|
|
90
|
+
*/
|
|
91
|
+
async function runStatus(opts = {}) {
|
|
92
|
+
const log = opts.log || ((s) => process.stdout.write(s + "\n"))
|
|
93
|
+
const config = opts.config !== undefined ? opts.config : readElliotConfig()
|
|
94
|
+
|
|
95
|
+
if (!config) {
|
|
96
|
+
log("Not configured. Copy the setup command from the Elliot portal and run it.")
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
log("")
|
|
101
|
+
log("Elliot-AI status")
|
|
102
|
+
log("─────────────────────────────")
|
|
103
|
+
log(`Workspace: ${config.org_name || "(unknown)"}`)
|
|
104
|
+
// The id, not just the name: it is the value the portal URLs and the setup
|
|
105
|
+
// command both carry, so it is what makes the CLI and the web app comparable
|
|
106
|
+
// at a glance.
|
|
107
|
+
log(`Workspace ID: ${config.tenant_id || "(unknown)"}`)
|
|
108
|
+
if (config.stack) log(`Stack: ${config.stack}`)
|
|
109
|
+
log(`Backend: ${config.backend_url || "(unset)"}`)
|
|
110
|
+
|
|
111
|
+
const healthy = config.backend_url ? await checkBackendHealth(config.backend_url) : false
|
|
112
|
+
log(`Reachable: ${healthy ? "yes" : "no"}`)
|
|
113
|
+
|
|
114
|
+
log("─────────────────────────────")
|
|
115
|
+
log("Connectors:")
|
|
116
|
+
const connectors = healthy ? await fetchConnectors(config) : []
|
|
117
|
+
if (!connectors.length) {
|
|
118
|
+
// Deliberately not "no connectors": an unreachable backend and an
|
|
119
|
+
// unconfigured workspace produce the same empty list, and saying which is
|
|
120
|
+
// which is the whole value of the line.
|
|
121
|
+
log(healthy ? " (none reported)" : " (backend unreachable — cannot tell)")
|
|
122
|
+
} else {
|
|
123
|
+
for (const c of connectors) {
|
|
124
|
+
const state = c.status === "connected" ? "connected" : c.status || "unknown"
|
|
125
|
+
log(` ${displayName(c.provider).padEnd(12)} ${state}`)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
log("")
|
|
129
|
+
|
|
130
|
+
return healthy ? 0 : 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { runStatus, readElliotConfig, checkBackendHealth, fetchConnectors, displayName }
|
package/lib/token-env.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Inject
|
|
5
|
-
* inherits, so its `elliot` provider (configured by `
|
|
6
|
-
* `apiKey: "{env:ELLIOT_TOKEN}"`) authenticates against
|
|
4
|
+
* Inject LiteLLM's shared master key into the environment the OpenCode
|
|
5
|
+
* runtime inherits, so its `elliot` provider (configured by `eai setup` to
|
|
6
|
+
* use `apiKey: "{env:ELLIOT_TOKEN}"`) authenticates against LiteLLM directly.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* The env var name (ELLIOT_TOKEN) is unchanged, but what it holds is not: it
|
|
9
|
+
* used to be the user's own per-instance JWT (`jwt_token`), revocable and
|
|
10
|
+
* scoped to them; it is now `litellm_master_key`, ONE secret shared by the
|
|
11
|
+
* whole gateway (LiteLLM is deliberately DB-less — no per-tenant credential
|
|
12
|
+
* is possible without one). `eai setup` writes it into
|
|
13
|
+
* ~/.elliot/config.json's `litellm_master_key` field verbatim, since the
|
|
14
|
+
* backend's `/onboarding/config` response includes it directly for exactly
|
|
15
|
+
* this purpose.
|
|
16
|
+
*
|
|
17
|
+
* ~/.elliot/config.json is the single source of truth; `eai setup` refreshes
|
|
18
|
+
* it there. We only READ it here — no network, no writes, so this adds no
|
|
19
|
+
* latency and cannot corrupt the config.
|
|
12
20
|
*
|
|
13
21
|
* Entirely best-effort: a missing/unreadable/corrupt config, or an env that
|
|
14
22
|
* already sets ELLIOT_TOKEN, leaves things exactly as they were. The launcher's
|
|
@@ -20,27 +28,55 @@ const os = require("os")
|
|
|
20
28
|
const path = require("path")
|
|
21
29
|
|
|
22
30
|
/**
|
|
31
|
+
* TWO CREDENTIALS, TWO HOPS. They are not interchangeable and the difference
|
|
32
|
+
* is the whole reason both exist:
|
|
33
|
+
*
|
|
34
|
+
* ELLIOT_TOKEN the LiteLLM key. Authenticates this machine TO the
|
|
35
|
+
* gateway, and is consumed there — LiteLLM strips it.
|
|
36
|
+
* Used by the model path and as the MCP entry's
|
|
37
|
+
* `Authorization`.
|
|
38
|
+
* ELLIOT_MCP_TOKEN the Elliot MCP credential. LiteLLM forwards it as
|
|
39
|
+
* Elliot's own `Authorization`, and it is what carries
|
|
40
|
+
* tenant, user and the CLI surface. Minted by
|
|
41
|
+
* `/onboarding/config` with the MCP audience.
|
|
42
|
+
*
|
|
43
|
+
* Putting the MCP credential in `Authorization` would authenticate it to
|
|
44
|
+
* LiteLLM as an unknown key; putting the LiteLLM key in `x-mcp-auth` would
|
|
45
|
+
* reach Elliot as a credential it cannot decode. Each is useless in the
|
|
46
|
+
* other's place, which is why they are separate variables rather than one.
|
|
47
|
+
*
|
|
23
48
|
* @param {NodeJS.ProcessEnv} env the environment to mutate (defaults to process.env)
|
|
24
49
|
* @returns {boolean} true iff ELLIOT_TOKEN was set from the config file
|
|
25
50
|
*/
|
|
26
51
|
function injectToken(env) {
|
|
27
52
|
const target = env || process.env
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
// `eai` at a specific token without touching the config file.
|
|
31
|
-
if (target.ELLIOT_TOKEN) return false
|
|
32
|
-
|
|
54
|
+
let config = null
|
|
33
55
|
try {
|
|
34
56
|
const configPath = path.join(os.homedir(), ".elliot", "config.json")
|
|
35
|
-
|
|
36
|
-
const config = JSON.parse(raw)
|
|
37
|
-
if (config && typeof config.jwt_token === "string" && config.jwt_token) {
|
|
38
|
-
target.ELLIOT_TOKEN = config.jwt_token
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
57
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
|
|
41
58
|
} catch {
|
|
42
59
|
// No config yet (setup not run), unreadable, or malformed — the runtime
|
|
43
60
|
// will simply have no elliot token, which is the pre-existing behaviour.
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Set INDEPENDENTLY of ELLIOT_TOKEN below. An explicit gateway key in the
|
|
65
|
+
// environment is a deliberate override of the gateway hop and says nothing
|
|
66
|
+
// about Elliot identity, so it must not also suppress the MCP credential —
|
|
67
|
+
// doing so would leave MCP silently unauthenticated whenever someone pointed
|
|
68
|
+
// `eai` at a different LiteLLM key.
|
|
69
|
+
if (!target.ELLIOT_MCP_TOKEN && config && typeof config.mcp_token === "string" && config.mcp_token) {
|
|
70
|
+
target.ELLIOT_MCP_TOKEN = config.mcp_token
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// An explicit ELLIOT_TOKEN in the environment wins — it lets a caller point
|
|
74
|
+
// `eai` at a specific token without touching the config file.
|
|
75
|
+
if (target.ELLIOT_TOKEN) return false
|
|
76
|
+
|
|
77
|
+
if (config && typeof config.litellm_master_key === "string" && config.litellm_master_key) {
|
|
78
|
+
target.ELLIOT_TOKEN = config.litellm_master_key
|
|
79
|
+
return true
|
|
44
80
|
}
|
|
45
81
|
return false
|
|
46
82
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The detached half of the update notification.
|
|
5
|
+
*
|
|
6
|
+
* Spawned by `maybeNotify`, at most once a day, with stdio ignored and the
|
|
7
|
+
* handle unref'd — so it cannot print anything, cannot hold the parent open,
|
|
8
|
+
* and cannot fail in a way the user sees. Its only job is to leave the answer
|
|
9
|
+
* in the cache for the NEXT run to report.
|
|
10
|
+
*
|
|
11
|
+
* It is a separate file rather than an inline `-e` script so that the code is
|
|
12
|
+
* readable, testable and visible in a stack trace, and so a future change does
|
|
13
|
+
* not have to be escaped through a command line.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { latestPublished, readState, writeState, notificationsDisabled } = require("./update.js")
|
|
17
|
+
|
|
18
|
+
if (notificationsDisabled(process.env)) process.exit(0)
|
|
19
|
+
|
|
20
|
+
latestPublished()
|
|
21
|
+
.then((latest) => {
|
|
22
|
+
if (latest) writeState({ ...readState(), latest, checkedAt: Date.now() })
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {
|
|
25
|
+
// Silence is the contract. The parent already stamped checkedAt, so a
|
|
26
|
+
// failure here just means the answer is refreshed tomorrow instead.
|
|
27
|
+
})
|