aegis-desktop 0.7.6 → 0.7.8
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 +103 -17
- package/lib/local/endpoints.js +228 -0
- package/lib/local/engine.js +253 -7
- package/lib/settings.js +42 -1
- package/package.json +4 -3
- package/renderer/app.js +282 -118
- package/renderer/index.html +1 -0
- package/renderer/preset-fill.js +79 -0
- package/renderer/stream-policy.js +65 -1
- package/renderer/transcript-view.js +5 -0
- package/renderer/usage.js +42 -11
- package/vendor/aegis.js +21 -3
package/README.md
CHANGED
|
@@ -5,26 +5,95 @@ with an **agentic tool loop**: the model can read, write, and edit files,
|
|
|
5
5
|
list directories, glob, grep, run shell commands in a persistent session, and
|
|
6
6
|
delegate whole sub-tasks to subagents. It does **not** require Claude Code.
|
|
7
7
|
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Download a build from the
|
|
11
|
+
[releases page](https://github.com/aegiscloud/aegiscode-desktop/releases), or
|
|
12
|
+
install from npm:
|
|
13
|
+
|
|
8
14
|
```bash
|
|
9
|
-
npm install -g aegis-desktop
|
|
10
|
-
aegis
|
|
15
|
+
npm install -g aegis-desktop # requires Node 18+
|
|
16
|
+
aegis # launch
|
|
11
17
|
```
|
|
12
18
|
|
|
19
|
+
Either way, on first launch:
|
|
20
|
+
|
|
21
|
+
1. Open **Settings** in the sidebar.
|
|
22
|
+
2. Paste an AEGIS key into the **API key** row — get a free one at
|
|
23
|
+
<https://aegiscloud.org>. `AEGIS_API_KEY` is picked up from the environment
|
|
24
|
+
if you would rather not paste it.
|
|
25
|
+
3. Pick a model class from the picker at the bottom of the composer.
|
|
26
|
+
|
|
27
|
+
The published package is [`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop)
|
|
28
|
+
on npm; this repo is its source. No AEGIS account is needed for the **Ollama**
|
|
29
|
+
class or a **local** custom endpoint — every other class bills your AEGIS
|
|
30
|
+
account, either the pooled margin or the BYOK handling fee.
|
|
31
|
+
|
|
32
|
+
## Using it
|
|
33
|
+
|
|
34
|
+
Everything is in the window — there is no slash-command line to learn. Plain
|
|
35
|
+
text in the composer is a prompt.
|
|
36
|
+
|
|
37
|
+
| Where | What it does |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **Composer** | Type and press `Enter`. `Shift+Enter` for a newline. |
|
|
40
|
+
| **Class picker** | Bottom of the composer — switches the route mid-conversation, context intact. |
|
|
41
|
+
| **Model dropdown** | Next to it — pins a model id for the selected class, or leaves it on "server default (auto)". |
|
|
42
|
+
| **Settings** (sidebar) | API key, provider keys, tool-confirmation toggle. |
|
|
43
|
+
| **Queue card** (sidebar) | The unattended work queue — same file the CLI drains. |
|
|
44
|
+
| **Quick Launcher card** | Enable/rebind the global hotkey. |
|
|
45
|
+
| **remember** (on any reply) | Pin that message to cross-machine cloud memory. |
|
|
46
|
+
|
|
47
|
+
Approval cards appear inline before `exec`, `writeFile` or `editFile` runs:
|
|
48
|
+
**Allow once**, **Allow for this session**, or **Deny**. Turn them off entirely
|
|
49
|
+
with **Settings → "Confirm before running tools"** — on by default.
|
|
50
|
+
|
|
13
51
|
## Model classes
|
|
14
52
|
|
|
15
|
-
Pick any of
|
|
53
|
+
Pick any of five transports from the model-class picker, switchable
|
|
16
54
|
mid-conversation with context intact:
|
|
17
55
|
|
|
18
|
-
| Class | Transport | Key held in |
|
|
19
|
-
|
|
20
|
-
| **Aegis Cloud** | `aegiscloud.org` — one entry, **Nexus**; the pool auto-routes across whichever providers are live | main process |
|
|
21
|
-
| **Ollama** | local `ollama` daemon | no key needed |
|
|
22
|
-
| **Custom OpenAI-compatible** (LM Studio,
|
|
23
|
-
| **Anthropic-compatible** (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
| Class | Transport | Key held in | Billed? |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| **Aegis Cloud** | `aegiscloud.org` — one entry, **Nexus**; the pool auto-routes across whichever providers are live | main process | yes — pooled margin |
|
|
59
|
+
| **Ollama** | local `ollama` daemon | no key needed | **no** — nothing leaves the machine |
|
|
60
|
+
| **Custom OpenAI-compatible** (LM Studio, vLLM, llama.cpp, an Ollama shim) | direct from the desktop app, **local base URLs only** | main process — never sent to the renderer | **no** — free lane, local only |
|
|
61
|
+
| **Anthropic-compatible** (a local Messages-format gateway — LiteLLM, claude-code-router) | direct from the desktop app, **local base URLs only** | main process | **no** — free lane, local only |
|
|
62
|
+
| **Bring your own key** | your provider key, relayed by AEGIS — see below | main process | yes — flat AEGIS handling fee |
|
|
63
|
+
|
|
64
|
+
Get a free AEGIS key at **https://aegiscloud.org**. The two custom classes are
|
|
65
|
+
free because aegiscode talks straight to an endpoint on your own machine; point
|
|
66
|
+
one at a hosted provider instead and there is nothing for AEGIS to meter, so
|
|
67
|
+
those models live on the **Bring your own key** class, which does bill.
|
|
68
|
+
|
|
69
|
+
### Bring your own key (BYOK)
|
|
70
|
+
|
|
71
|
+
For the providers AEGIS does **not** run in its pool — bring your own key and
|
|
72
|
+
the models that key unlocks appear as their own entries, one per provider.
|
|
73
|
+
|
|
74
|
+
1. **Settings** → find the row named `BYOK: <Provider>` (OpenAI, Anthropic,
|
|
75
|
+
DeepSeek, Groq, xAI, Mistral, Gemini, OpenRouter, …).
|
|
76
|
+
2. Paste your provider key and **Save**. There is no base-URL field on these
|
|
77
|
+
rows: a BYOK turn always talks to AEGIS's own relay
|
|
78
|
+
(`/api/v1/byok/chat/completions`), which is what attaches your AEGIS key.
|
|
79
|
+
3. Select the **Bring your own key** class and pick a model.
|
|
80
|
+
|
|
81
|
+
**What it costs.** AEGIS pays your provider nothing on this lane, so there is no
|
|
82
|
+
provider cost to take a margin on — instead your AEGIS account is charged a flat
|
|
83
|
+
**handling fee** per 1k tokens, for the routing, prompt assembly, caching, tool
|
|
84
|
+
bridging and uptime that still happen server-side. The rate is the server's own
|
|
85
|
+
(published on `GET /api/v1/byok/providers`) and is shown in Settings directly
|
|
86
|
+
under the provider rows; it is never hardcoded here, so it cannot drift from the
|
|
87
|
+
ledger that bills you. It is deliberately below the pooled price for the same
|
|
88
|
+
traffic — BYOK stays the cheaper lane, it just is not the free one.
|
|
89
|
+
|
|
90
|
+
**Two keys are needed.** Your provider key *and* an AEGIS account key: the
|
|
91
|
+
handling fee has to be billed somewhere. With no AEGIS key connected the class
|
|
92
|
+
shows every model but says exactly that, rather than failing opaquely.
|
|
93
|
+
|
|
94
|
+
Note that a **BYOK turn is single-shot** — the relay takes no `tools` parameter,
|
|
95
|
+
so the agentic tool loop below is off for these models. Use a pooled or direct
|
|
96
|
+
class when you want file and shell access.
|
|
28
97
|
|
|
29
98
|
## Tools available to the model
|
|
30
99
|
|
|
@@ -180,7 +249,16 @@ The app registers an `aegis://` protocol handler:
|
|
|
180
249
|
## Run from source
|
|
181
250
|
|
|
182
251
|
```bash
|
|
183
|
-
|
|
252
|
+
git clone https://github.com/aegiscloud/aegiscode-desktop.git
|
|
253
|
+
cd aegiscode-desktop
|
|
254
|
+
npm install
|
|
255
|
+
npm start
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Or, from the monorepo checkout, run this directory directly:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
cd aegiscode-plugin/desktop
|
|
184
262
|
npm install
|
|
185
263
|
npm start
|
|
186
264
|
```
|
|
@@ -217,6 +295,14 @@ bin/aegis.js `aegis` CLI entry point for the global npm install
|
|
|
217
295
|
This directory is part of the [aegiscode-plugin](../README.md) monorepo,
|
|
218
296
|
which also ships a Claude Code plugin and the shared `client/aegis.js`
|
|
219
297
|
transport over the same AEGIS backend — see the repo root for that fuller
|
|
220
|
-
architecture picture
|
|
221
|
-
|
|
222
|
-
|
|
298
|
+
architecture picture, and [cli/README.md](../cli/README.md) for the terminal
|
|
299
|
+
host over the same engine.
|
|
300
|
+
|
|
301
|
+
**Two repos, one product.** This directory is the source of truth. The
|
|
302
|
+
standalone repo at
|
|
303
|
+
[aegiscloud/aegiscode-desktop](https://github.com/aegiscloud/aegiscode-desktop)
|
|
304
|
+
is a `git subtree split` of it — same code, published separately so it can be
|
|
305
|
+
cloned and built on its own. Edits land here and are synced out; nothing is
|
|
306
|
+
authored there. The npm package
|
|
307
|
+
[`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from
|
|
308
|
+
that standalone repo.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The direct-dial policy: which base URLs may reach a provider transport, and
|
|
5
|
+
* why that is the only free lane left.
|
|
6
|
+
*
|
|
7
|
+
* Both hosts that can talk to a user-supplied endpoint — the desktop app
|
|
8
|
+
* (settings pane → 'openai-compat' / 'anthropic' classes) and the CLI
|
|
9
|
+
* (src/engine.js prepareCustom, the `/model add` catalog) — share THIS file
|
|
10
|
+
* rather than a copy each. The CLI vendors `desktop/lib/local/` wholesale and
|
|
11
|
+
* resolves this module through src/sharedpaths.js, so the two hosts cannot
|
|
12
|
+
* drift apart about what "local" means or which error a remote URL gets. A
|
|
13
|
+
* second implementation is how one host would keep billing correctly while the
|
|
14
|
+
* other quietly served unpaid traffic.
|
|
15
|
+
*
|
|
16
|
+
* The rule: every lane that can bill does. `aegis` is the pooled route (the
|
|
17
|
+
* account key is attached and the pool takes its margin); `byok` is the relay
|
|
18
|
+
* (`services/pricing.price_byok_call` → `token_bank.charge_byok`, the AEGIS
|
|
19
|
+
* handling fee, charged against the account resolved from `X-AEGIS-Key`). The
|
|
20
|
+
* provider transports in providers.js bill NOTHING — they are a direct dial to
|
|
21
|
+
* whoever owns the URL — so the only usage they may carry is an endpoint on
|
|
22
|
+
* this machine, where there is no vendor to pay in the first place.
|
|
23
|
+
*
|
|
24
|
+
* That is why a remote base URL is refused rather than metered: the relay
|
|
25
|
+
* accepts a fixed catalog of provider ids (`services/nexus_provider/catalog.py`,
|
|
26
|
+
* upstream key read from `X-Provider-Key`), so an arbitrary remote URL has
|
|
27
|
+
* nothing to be billed against even if a client wanted to invoice it. Remote
|
|
28
|
+
* providers belong on BYOK, which bills. There is deliberately no flag or env
|
|
29
|
+
* var that re-opens the direct lane for a remote URL, because such a flag would
|
|
30
|
+
* be a billing bypass.
|
|
31
|
+
*
|
|
32
|
+
* Where the gate lives — the seams, not the transport:
|
|
33
|
+
* · desktop/lib/settings.js `set()` refuses to STORE a remote base URL, so
|
|
34
|
+
* the unusable configuration cannot be created.
|
|
35
|
+
* · desktop/lib/local/engine.js `chat()` refuses to DIAL one before any
|
|
36
|
+
* transport call (a row hand-edited into the
|
|
37
|
+
* settings file, or written before this policy
|
|
38
|
+
* existed, still cannot be used).
|
|
39
|
+
* · cli/src/custommodels.js refuses one at `/model add` and again at
|
|
40
|
+
* dispatch, for the same two reasons.
|
|
41
|
+
* providers.js itself stays policy-free: it is a wire-format function, and
|
|
42
|
+
* pinning the policy there would make it unreachable for the tests that check
|
|
43
|
+
* URL building. Same split the BYOK balance gate uses — the relay owns the
|
|
44
|
+
* decision, the client refuses to send.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** Host of a URL, lower-cased, or '' if it does not parse. */
|
|
48
|
+
function hostOf(url) {
|
|
49
|
+
try {
|
|
50
|
+
return new URL(String(url)).host.toLowerCase();
|
|
51
|
+
} catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether a base URL addresses an endpoint on this machine — the gate that
|
|
58
|
+
* decides whether the free direct lane is available at all.
|
|
59
|
+
*
|
|
60
|
+
* True for: loopback (localhost, `*.localhost`, 127.0.0.0/8, `::1`), RFC1918
|
|
61
|
+
* private ranges (10/8, 172.16/12, 192.168/16), IPv6 unique-local fc00::/7,
|
|
62
|
+
* link-local (169.254/16, fe80::/10), the reserved local suffixes `.local`,
|
|
63
|
+
* `.internal`, `.lan`, and a bare dotless hostname (`ollama`, `gpu-box` — it
|
|
64
|
+
* can only resolve through this machine's own resolver).
|
|
65
|
+
*
|
|
66
|
+
* FAIL CLOSED: anything that does not parse, carries no host, is a public
|
|
67
|
+
* address or name, or is merely ambiguous is NOT local. That direction is the
|
|
68
|
+
* whole point — a false "local" is an unpaid turn, a false "remote" is a
|
|
69
|
+
* refusal the user can fix by pointing at a local address or by moving to the
|
|
70
|
+
* billed lane, which is the outcome we want in a tie.
|
|
71
|
+
*/
|
|
72
|
+
function isLocalEndpoint(url) {
|
|
73
|
+
let host = '';
|
|
74
|
+
let scheme = '';
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(String(url == null ? '' : url));
|
|
77
|
+
host = parsed.hostname.toLowerCase();
|
|
78
|
+
scheme = parsed.protocol.toLowerCase();
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// Only a base URL a transport can actually dial. The transports in
|
|
83
|
+
// providers.js append '/chat/completions' and fetch it, so anything that is
|
|
84
|
+
// not http(s) — or has no scheme at all — is not an endpoint, and is refused
|
|
85
|
+
// by the same fail-closed rule as an unparseable value. Checked before the
|
|
86
|
+
// host, so `ftp://box.local` cannot ride the local suffix to a pass.
|
|
87
|
+
if (scheme !== 'http:' && scheme !== 'https:') return false;
|
|
88
|
+
if (!host) return false;
|
|
89
|
+
// WHATWG keeps the brackets on an IPv6 hostname; strip them so one spelling
|
|
90
|
+
// covers both `[::1]` and a bare `::1`.
|
|
91
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
92
|
+
if (!host || /[\s/\\@]/.test(host)) return false;
|
|
93
|
+
|
|
94
|
+
// Loopback by name, plus the reserved local suffixes. `.local`/`.internal`/
|
|
95
|
+
// `.lan` are the mDNS / split-DNS names an on-box service answers to. The
|
|
96
|
+
// suffix must END the name: `example.local.evil.com` is somebody else's host.
|
|
97
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true;
|
|
98
|
+
if (/\.(local|internal|lan)$/.test(host)) return true;
|
|
99
|
+
|
|
100
|
+
// IPv6 literals.
|
|
101
|
+
if (host.includes(':')) {
|
|
102
|
+
if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true; // loopback
|
|
103
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host)) return true; // fc00::/7 unique-local
|
|
104
|
+
if (/^fe[89ab][0-9a-f]:/.test(host)) return true; // fe80::/10 link-local
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// IPv4 literals.
|
|
109
|
+
const quad = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
110
|
+
if (quad) {
|
|
111
|
+
const o = quad.slice(1).map(Number);
|
|
112
|
+
if (o.some((n) => n > 255)) return false; // not an address, and not local
|
|
113
|
+
const [a, b] = o;
|
|
114
|
+
if (a === 127) return true; // 127.0.0.0/8 loopback
|
|
115
|
+
if (a === 10) return true; // 10.0.0.0/8
|
|
116
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
|
117
|
+
if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
|
118
|
+
if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local
|
|
119
|
+
return false; // a real, routable address — remote, and billed
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A bare, dotless hostname is local by convention (`ollama`, `llama-box`):
|
|
123
|
+
// it can only resolve through this machine's own resolver or /etc/hosts.
|
|
124
|
+
// Anything with a dot is a DNS name for somebody else's machine.
|
|
125
|
+
return !host.includes('.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What IS allowed here, in one sentence, shared by both hosts' refusals. */
|
|
129
|
+
const ALLOWED_HINT =
|
|
130
|
+
'Allowed: localhost, 127.0.0.1, a private/LAN address (10.x, 172.16-31.x, 192.168.x), ' +
|
|
131
|
+
'*.local/.internal/.lan, or a dotless host like "ollama".';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The refusal a non-local base URL gets, in the words the user needs in order
|
|
135
|
+
* to act: it names the offending URL, the kind of address that IS allowed, and
|
|
136
|
+
* the lane that replaces this one. `hint` is the host-specific half —
|
|
137
|
+
* `/class byok` in the terminal, the Bring-your-own-key Settings row in the
|
|
138
|
+
* GUI — while the policy half is shared, so the two hosts cannot describe the
|
|
139
|
+
* same rule differently.
|
|
140
|
+
*/
|
|
141
|
+
function remoteRefusal(baseURL, { subject = 'custom endpoints', hint = '' } = {}) {
|
|
142
|
+
const where = String(baseURL == null ? '' : baseURL);
|
|
143
|
+
const tail = hint || 'Remote providers are billed, so use the lane that bills them, or point this at a local address.';
|
|
144
|
+
return `${subject} must be LOCAL — remote base URL ${JSON.stringify(where)} is not offered on this lane. ` +
|
|
145
|
+
`${ALLOWED_HINT} ${tail}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Throw the refusal (as a 400-class error every caller already paints) unless
|
|
150
|
+
* the base URL is local. The one-line form of the gate for the dispatch and
|
|
151
|
+
* storage seams, so neither has to remember the status code.
|
|
152
|
+
*
|
|
153
|
+
* An EMPTY base URL is refused here like any other non-local value, because
|
|
154
|
+
* there is no endpoint to dial. The storage seam does not use this function
|
|
155
|
+
* directly for that reason — see allowsDirectDialRow below, which treats "no
|
|
156
|
+
* URL configured" as "nothing to gate".
|
|
157
|
+
*/
|
|
158
|
+
function ensureLocalEndpoint(baseURL, opts) {
|
|
159
|
+
if (isLocalEndpoint(baseURL)) return true;
|
|
160
|
+
const err = new Error(remoteRefusal(baseURL, opts));
|
|
161
|
+
err.status = 400;
|
|
162
|
+
err.code = 'CUSTOM_ENDPOINT_NOT_LOCAL';
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The settings-store rows whose base URL is dialled DIRECTLY by the transports
|
|
168
|
+
* in providers.js — i.e. the rows the gate above exists for.
|
|
169
|
+
*
|
|
170
|
+
* 'openai-compat', 'anthropic' the desktop's two custom endpoints (its
|
|
171
|
+
* Settings pane names them exactly so, and
|
|
172
|
+
* engine.js's CUSTOM_CLASSES reads the row
|
|
173
|
+
* under the class name).
|
|
174
|
+
* 'custom:*' the CLI's per-model namespace (src/
|
|
175
|
+
* custommodels.js). The CLI keeps the base URL
|
|
176
|
+
* in config.json `customModels` and stores only
|
|
177
|
+
* the KEY here, so this prefix is a second lock
|
|
178
|
+
* on a door that is already shut — deliberate:
|
|
179
|
+
* a future writer that puts a URL in this row
|
|
180
|
+
* must not silently reopen the lane.
|
|
181
|
+
*
|
|
182
|
+
* It matters that this is a NAMED list and not "every row". The same store
|
|
183
|
+
* holds the billed lanes' rows, and those legitimately carry remote values or
|
|
184
|
+
* nothing at all:
|
|
185
|
+
* · `byok:<provider>` — always remote by definition (the provider's own API,
|
|
186
|
+
* reached through AEGIS's relay, which is what bills it). The desktop's
|
|
187
|
+
* Settings pane saves those rows with baseURL '' (the relay owns the URL),
|
|
188
|
+
* and a blanket "must be local" rule would refuse even the empty string,
|
|
189
|
+
* breaking BYOK configuration entirely.
|
|
190
|
+
* · `aegis` — reserved namespace, never touched through this surface.
|
|
191
|
+
* So a gate keyed on the value alone is wrong; it is keyed on the ROW.
|
|
192
|
+
*/
|
|
193
|
+
const DIRECT_DIAL_ROWS = Object.freeze(['openai-compat', 'anthropic']);
|
|
194
|
+
const DIRECT_DIAL_PREFIXES = Object.freeze(['custom:']);
|
|
195
|
+
|
|
196
|
+
/** Whether a settings-store row's base URL is dialled directly (and so gated). */
|
|
197
|
+
function isDirectDialRow(provider) {
|
|
198
|
+
const p = String(provider == null ? '' : provider);
|
|
199
|
+
if (DIRECT_DIAL_ROWS.includes(p)) return true;
|
|
200
|
+
return DIRECT_DIAL_PREFIXES.some((pre) => p.startsWith(pre));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether a row may STORE this base URL — the storage seam's question, which is
|
|
205
|
+
* not quite the dispatch seam's.
|
|
206
|
+
*
|
|
207
|
+
* A direct-dial row with no URL is fine to store: that is how a row is cleared,
|
|
208
|
+
* and how it looks before it is configured (`customStatus` reports it as
|
|
209
|
+
* unconfigured, so nothing dials it). What must never be stored is a NON-EMPTY
|
|
210
|
+
* remote URL, which would make the row look configured and usable.
|
|
211
|
+
*/
|
|
212
|
+
function allowsDirectDialRow(provider, baseURL) {
|
|
213
|
+
if (!isDirectDialRow(provider)) return true;
|
|
214
|
+
const url = String(baseURL == null ? '' : baseURL).trim();
|
|
215
|
+
if (!url) return true; // clearing the row, or a not-yet-configured one
|
|
216
|
+
return isLocalEndpoint(url);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
hostOf,
|
|
221
|
+
isLocalEndpoint,
|
|
222
|
+
remoteRefusal,
|
|
223
|
+
ensureLocalEndpoint,
|
|
224
|
+
isDirectDialRow,
|
|
225
|
+
allowsDirectDialRow,
|
|
226
|
+
ALLOWED_HINT,
|
|
227
|
+
DIRECT_DIAL_ROWS,
|
|
228
|
+
};
|