@davvidbaker/flambe-cli 0.1.1 → 0.1.2
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 +4 -2
- package/package.json +1 -1
- package/src/cli.mjs +90 -7
- package/src/client.mjs +209 -17
- package/src/local/commands.mjs +291 -0
- package/src/local/http.mjs +76 -10
- package/src/local/serve.mjs +2 -1
- package/src/local/store.mjs +117 -31
- package/src/questions.mjs +48 -0
- package/src/queue.mjs +15 -3
- package/src/runtime.mjs +23 -0
package/src/local/store.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
|
|
6
6
|
import { pickName, usableName } from './agents.mjs';
|
|
7
7
|
|
|
8
|
-
const LIFECYCLE_PHASES = ['B', 'E', 'S', 'R'];
|
|
9
|
-
const OPEN_PHASES = ['B', 'R'];
|
|
8
|
+
const LIFECYCLE_PHASES = ['B', 'E', 'S', 'R', 'X', 'J', 'V'];
|
|
9
|
+
const OPEN_PHASES = ['B', 'R', 'X'];
|
|
10
10
|
|
|
11
11
|
const SCHEMA = `
|
|
12
12
|
PRAGMA foreign_keys = ON;
|
|
@@ -60,6 +60,10 @@ CREATE TABLE IF NOT EXISTS activities (
|
|
|
60
60
|
weight INTEGER,
|
|
61
61
|
agent_id TEXT,
|
|
62
62
|
agent_name TEXT,
|
|
63
|
+
scheduled_start_ms INTEGER,
|
|
64
|
+
scheduled_end_ms INTEGER,
|
|
65
|
+
proposed_by_agent_id TEXT,
|
|
66
|
+
proposed_by_agent_name TEXT,
|
|
63
67
|
export_id TEXT NOT NULL UNIQUE
|
|
64
68
|
);
|
|
65
69
|
|
|
@@ -79,13 +83,6 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
79
83
|
export_id TEXT NOT NULL UNIQUE
|
|
80
84
|
);
|
|
81
85
|
|
|
82
|
-
CREATE TABLE IF NOT EXISTS todos (
|
|
83
|
-
id INTEGER PRIMARY KEY,
|
|
84
|
-
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
85
|
-
name TEXT NOT NULL,
|
|
86
|
-
description TEXT
|
|
87
|
-
);
|
|
88
|
-
|
|
89
86
|
CREATE TABLE IF NOT EXISTS mantras (
|
|
90
87
|
id INTEGER PRIMARY KEY,
|
|
91
88
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
@@ -142,11 +139,25 @@ export const EXPORT_FORMAT = 'flambe-local-export';
|
|
|
142
139
|
export const EXPORT_VERSION = 1;
|
|
143
140
|
|
|
144
141
|
export const DEFAULT_CATEGORIES = [
|
|
145
|
-
{ name: '
|
|
146
|
-
{ name: '
|
|
147
|
-
{ name: '
|
|
148
|
-
{ name: '
|
|
149
|
-
{ name: '
|
|
142
|
+
{ name: 'bug fixing', color_background: '#ff4747', color_text: '#000000' },
|
|
143
|
+
{ name: 'research', color_background: '#cd94f1', color_text: '#000000' },
|
|
144
|
+
{ name: 'cleaning', color_background: '#ffbd69', color_text: '#000000' },
|
|
145
|
+
{ name: 'bug hunting', color_background: '#ef60ab', color_text: '#000000' },
|
|
146
|
+
{ name: 'design', color_background: '#dce7ea', color_text: '#000000' },
|
|
147
|
+
{ name: 'writing tests', color_background: '#50f035', color_text: '#000000' },
|
|
148
|
+
{ name: 'toil', color_background: '#8b6125', color_text: '#ffffff' },
|
|
149
|
+
{ name: 'analytics', color_background: '#f9f9f9', color_text: '#4598d6' },
|
|
150
|
+
{ name: 'oss', color_background: '#60ef6c', color_text: '#000000' },
|
|
151
|
+
{ name: 'enhancements', color_background: '#55defb', color_text: '#ffffff' },
|
|
152
|
+
{ name: 'refactoring components', color_background: '#ffd368', color_text: '#000000' },
|
|
153
|
+
{ name: 'Fundamentals', color_background: '#acefdb', color_text: '#000000' },
|
|
154
|
+
{ name: 'jarring ui', color_background: '#f22d6a', color_text: '#ffffff' },
|
|
155
|
+
{ name: 'writing', color_background: '#ffefce', color_text: '#000000' },
|
|
156
|
+
{ name: 'risky', color_background: '#ff2a22', color_text: '#ffffff' },
|
|
157
|
+
{ name: 'dependency upgrades', color_background: '#b58e38', color_text: '#ffffff' },
|
|
158
|
+
{ name: 'shiny', color_background: '#f5ff86', color_text: '#000000' },
|
|
159
|
+
{ name: 'performance', color_background: '#60efc7', color_text: '#000000' },
|
|
160
|
+
{ name: 'learning', color_background: '#d3f791', color_text: '#000000' },
|
|
150
161
|
];
|
|
151
162
|
|
|
152
163
|
export function hashToken(rawToken) {
|
|
@@ -169,6 +180,17 @@ export class LocalStore {
|
|
|
169
180
|
this.db = new DatabaseSync(dbPath);
|
|
170
181
|
this.db.exec('PRAGMA foreign_keys = ON');
|
|
171
182
|
this.db.exec(SCHEMA);
|
|
183
|
+
this.db.exec('DROP TABLE IF EXISTS todos');
|
|
184
|
+
for (const column of [
|
|
185
|
+
'scheduled_start_ms INTEGER',
|
|
186
|
+
'scheduled_end_ms INTEGER',
|
|
187
|
+
'proposed_by_agent_id TEXT',
|
|
188
|
+
'proposed_by_agent_name TEXT',
|
|
189
|
+
]) {
|
|
190
|
+
const name = column.split(' ')[0];
|
|
191
|
+
const exists = this.db.prepare('PRAGMA table_info(activities)').all().some(row => row.name === name);
|
|
192
|
+
if (!exists) this.db.exec(`ALTER TABLE activities ADD COLUMN ${column}`);
|
|
193
|
+
}
|
|
172
194
|
this.#seed();
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -317,6 +339,7 @@ export class LocalStore {
|
|
|
317
339
|
}
|
|
318
340
|
|
|
319
341
|
listCategories(userId) {
|
|
342
|
+
this.#ensureDefaultCategories(userId);
|
|
320
343
|
return this.db.prepare(
|
|
321
344
|
'SELECT id, name, color_background, color_text FROM categories WHERE user_id = ? ORDER BY id',
|
|
322
345
|
).all(userId);
|
|
@@ -383,7 +406,7 @@ export class LocalStore {
|
|
|
383
406
|
.all(userId);
|
|
384
407
|
}
|
|
385
408
|
|
|
386
|
-
/** Newest activity in the trace
|
|
409
|
+
/** Newest running activity in the trace, optionally for one agent or thread. */
|
|
387
410
|
newestActiveActivity(traceId, { agentId, threadId } = {}) {
|
|
388
411
|
const placeholders = LIFECYCLE_PHASES.map(() => '?').join(',');
|
|
389
412
|
const open = OPEN_PHASES.map(() => '?').join(',');
|
|
@@ -471,8 +494,11 @@ export class LocalStore {
|
|
|
471
494
|
: { agent_id: null, agent_name: null };
|
|
472
495
|
|
|
473
496
|
const inserted = this.db.prepare(`
|
|
474
|
-
INSERT INTO activities (
|
|
475
|
-
|
|
497
|
+
INSERT INTO activities (
|
|
498
|
+
thread_id, parent_id, name, description, weight, agent_id, agent_name,
|
|
499
|
+
scheduled_start_ms, scheduled_end_ms, proposed_by_agent_id, proposed_by_agent_name, export_id
|
|
500
|
+
)
|
|
501
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
476
502
|
`).run(
|
|
477
503
|
thread.id,
|
|
478
504
|
parentId,
|
|
@@ -481,6 +507,10 @@ export class LocalStore {
|
|
|
481
507
|
activity.weight ?? null,
|
|
482
508
|
agentColumns.agent_id,
|
|
483
509
|
agentColumns.agent_name,
|
|
510
|
+
activity.scheduled_start_ms ?? null,
|
|
511
|
+
activity.scheduled_end_ms ?? null,
|
|
512
|
+
activity.proposed_by_agent_id ?? agentColumns.agent_id,
|
|
513
|
+
activity.proposed_by_agent_name ?? agentColumns.agent_name,
|
|
484
514
|
randomUUID(),
|
|
485
515
|
);
|
|
486
516
|
const activityId = Number(inserted.lastInsertRowid);
|
|
@@ -510,6 +540,38 @@ export class LocalStore {
|
|
|
510
540
|
};
|
|
511
541
|
}
|
|
512
542
|
|
|
543
|
+
createUnstarted(userId, { traceId, name, description, weight, agent, scheduledStart, scheduledEnd }) {
|
|
544
|
+
const thread = this.#defaultThread(Number(traceId));
|
|
545
|
+
if (!thread) return { error: 'not_found' };
|
|
546
|
+
const trace = this.db.prepare('SELECT id FROM traces WHERE id = ? AND user_id = ?').get(thread.trace_id, userId);
|
|
547
|
+
if (!trace) return { error: 'not_found' };
|
|
548
|
+
const inserted = this.db.prepare(`
|
|
549
|
+
INSERT INTO activities (
|
|
550
|
+
thread_id, parent_id, name, description, weight, agent_id, agent_name,
|
|
551
|
+
scheduled_start_ms, scheduled_end_ms, proposed_by_agent_id, proposed_by_agent_name, export_id
|
|
552
|
+
) VALUES (?, NULL, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?)
|
|
553
|
+
`).run(
|
|
554
|
+
thread.id, name, description ?? null, weight ?? null,
|
|
555
|
+
scheduledStart ?? null, scheduledEnd ?? null,
|
|
556
|
+
agent?.agent_id ?? null, agent?.name ?? null, randomUUID(),
|
|
557
|
+
);
|
|
558
|
+
return { activity: { id: Number(inserted.lastInsertRowid), name, thread_id: thread.id, parent_id: null } };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
listUnstarted(traceId) {
|
|
562
|
+
return this.db.prepare(`
|
|
563
|
+
SELECT activities.* FROM activities
|
|
564
|
+
JOIN threads ON threads.id = activities.thread_id
|
|
565
|
+
WHERE threads.trace_id = ?
|
|
566
|
+
AND NOT EXISTS (
|
|
567
|
+
SELECT 1 FROM events
|
|
568
|
+
WHERE events.activity_id = activities.id
|
|
569
|
+
AND events.phase IN ('B','R','X','S','E','J','V')
|
|
570
|
+
)
|
|
571
|
+
ORDER BY activities.id
|
|
572
|
+
`).all(Number(traceId));
|
|
573
|
+
}
|
|
574
|
+
|
|
513
575
|
#defaultThread(traceId) {
|
|
514
576
|
return this.db.prepare('SELECT id, trace_id FROM threads WHERE trace_id = ? ORDER BY rank, id LIMIT 1').get(traceId) ?? null;
|
|
515
577
|
}
|
|
@@ -530,6 +592,8 @@ export class LocalStore {
|
|
|
530
592
|
weight: row.weight,
|
|
531
593
|
parent_id: row.parent_id,
|
|
532
594
|
thread_id: row.thread_id,
|
|
595
|
+
agent_id: row.agent_id ?? null,
|
|
596
|
+
agent_name: row.agent_name ?? null,
|
|
533
597
|
};
|
|
534
598
|
}
|
|
535
599
|
|
|
@@ -545,6 +609,13 @@ export class LocalStore {
|
|
|
545
609
|
WHERE id = ?
|
|
546
610
|
`).run(attrs.name ?? null, attrs.description ?? null, attrs.weight ?? null, attrs.parent_id ?? null, current.id);
|
|
547
611
|
|
|
612
|
+
if (Object.prototype.hasOwnProperty.call(attrs, 'agent_id')) {
|
|
613
|
+
const agentId = attrs.agent_id == null || attrs.agent_id === '' ? null : String(attrs.agent_id);
|
|
614
|
+
const agentName = agentId == null ? null : this.#activityAgentName(userId, agentId, attrs);
|
|
615
|
+
this.db.prepare('UPDATE activities SET agent_id = ?, agent_name = ? WHERE id = ?')
|
|
616
|
+
.run(agentId, agentName, current.id);
|
|
617
|
+
}
|
|
618
|
+
|
|
548
619
|
if (Array.isArray(attrs.category_ids)) {
|
|
549
620
|
this.db.prepare('DELETE FROM activities_categories WHERE activity_id = ?').run(current.id);
|
|
550
621
|
for (const categoryId of attrs.category_ids) {
|
|
@@ -628,7 +699,6 @@ export class LocalStore {
|
|
|
628
699
|
username: user.username,
|
|
629
700
|
traces: this.listTraces(userId),
|
|
630
701
|
categories: this.listCategories(userId),
|
|
631
|
-
todos: this.db.prepare('SELECT id, name, description FROM todos WHERE user_id = ?').all(userId),
|
|
632
702
|
mantras: this.db.prepare('SELECT id, name, timestamp_ms FROM mantras WHERE user_id = ? ORDER BY id').all(userId)
|
|
633
703
|
.map(row => ({ id: row.id, name: row.name, timestamp: isoFromMs(row.timestamp_ms) })),
|
|
634
704
|
attentionShifts: this.db.prepare(
|
|
@@ -654,17 +724,14 @@ export class LocalStore {
|
|
|
654
724
|
timestamp: isoFromMs(row.timestamp_ms),
|
|
655
725
|
})),
|
|
656
726
|
observations: this.listObservations(userId),
|
|
727
|
+
agents: this.listAgents(userId).map(agent => ({
|
|
728
|
+
agent_id: agent.agent_id,
|
|
729
|
+
name: agent.name,
|
|
730
|
+
platform: agent.platform ?? null,
|
|
731
|
+
})),
|
|
657
732
|
};
|
|
658
733
|
}
|
|
659
734
|
|
|
660
|
-
createTodo(userId, attrs) {
|
|
661
|
-
const result = this.db.prepare(
|
|
662
|
-
'INSERT INTO todos (user_id, name, description) VALUES (?, ?, ?)',
|
|
663
|
-
).run(userId, attrs.name, attrs.description ?? null);
|
|
664
|
-
return this.db.prepare('SELECT id, name, description FROM todos WHERE id = ?')
|
|
665
|
-
.get(Number(result.lastInsertRowid));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
735
|
createMantra(userId, attrs) {
|
|
669
736
|
const timestamp = Number(attrs.timestamp_integer ?? Date.now());
|
|
670
737
|
const result = this.db.prepare(
|
|
@@ -792,6 +859,24 @@ export class LocalStore {
|
|
|
792
859
|
};
|
|
793
860
|
}
|
|
794
861
|
|
|
862
|
+
#activityAgentName(userId, agentId, attrs) {
|
|
863
|
+
const known = this.db.prepare(
|
|
864
|
+
'SELECT name FROM agents WHERE user_id = ? AND agent_id = ?',
|
|
865
|
+
).get(userId, agentId);
|
|
866
|
+
if (known?.name) return known.name;
|
|
867
|
+
const snapshot = this.db.prepare(`
|
|
868
|
+
SELECT activities.agent_name FROM activities
|
|
869
|
+
JOIN threads ON threads.id = activities.thread_id
|
|
870
|
+
JOIN traces ON traces.id = threads.trace_id
|
|
871
|
+
WHERE traces.user_id = ? AND activities.agent_id = ?
|
|
872
|
+
AND activities.agent_name IS NOT NULL AND activities.agent_name != ''
|
|
873
|
+
LIMIT 1
|
|
874
|
+
`).get(userId, agentId);
|
|
875
|
+
if (snapshot?.agent_name) return snapshot.agent_name;
|
|
876
|
+
if (typeof attrs.agent_name === 'string' && attrs.agent_name !== '') return attrs.agent_name;
|
|
877
|
+
return agentId;
|
|
878
|
+
}
|
|
879
|
+
|
|
795
880
|
#eventPayload(event) {
|
|
796
881
|
const categoryIds = event.activity_id == null
|
|
797
882
|
? []
|
|
@@ -830,15 +915,16 @@ export class LocalStore {
|
|
|
830
915
|
}
|
|
831
916
|
|
|
832
917
|
#ensureDefaultCategories(userId) {
|
|
833
|
-
const
|
|
834
|
-
'SELECT
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
918
|
+
const have = new Set(
|
|
919
|
+
this.db.prepare('SELECT lower(name) AS name FROM categories WHERE user_id = ?')
|
|
920
|
+
.all(userId)
|
|
921
|
+
.map(row => row.name),
|
|
922
|
+
);
|
|
838
923
|
const insert = this.db.prepare(
|
|
839
924
|
'INSERT INTO categories (user_id, name, color_background, color_text, export_id) VALUES (?, ?, ?, ?, ?)',
|
|
840
925
|
);
|
|
841
926
|
for (const category of DEFAULT_CATEGORIES) {
|
|
927
|
+
if (have.has(category.name.toLowerCase())) continue;
|
|
842
928
|
insert.run(
|
|
843
929
|
userId,
|
|
844
930
|
category.name,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// A question the reducer asked and the worker has not answered yet (ADR-014).
|
|
7
|
+
// One open question per trace. The CLI reminds; it never refuses the next command.
|
|
8
|
+
|
|
9
|
+
export function defaultQuestionsPath() {
|
|
10
|
+
return join(homedir(), '.flambe', 'open-questions.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readOpenQuestion(traceId, path = defaultQuestionsPath()) {
|
|
14
|
+
if (!traceId) return undefined;
|
|
15
|
+
const stored = readAll(path)[String(traceId)];
|
|
16
|
+
if (!stored || typeof stored.reply !== 'string' || !stored.reply.trim()) return undefined;
|
|
17
|
+
return { activityId: stored.activityId, reply: stored.reply };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rememberQuestion(traceId, { activityId, reply }, path = defaultQuestionsPath()) {
|
|
21
|
+
if (!traceId || !reply) return;
|
|
22
|
+
const all = { ...readAll(path), [String(traceId)]: { activityId, reply } };
|
|
23
|
+
writeAll(path, all);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearQuestion(traceId, path = defaultQuestionsPath()) {
|
|
27
|
+
if (!traceId) return;
|
|
28
|
+
const all = readAll(path);
|
|
29
|
+
if (!(String(traceId) in all)) return;
|
|
30
|
+
delete all[String(traceId)];
|
|
31
|
+
writeAll(path, all);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readAll(path) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeAll(path, questions) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
46
|
+
writeFileSync(tmp, `${JSON.stringify(questions, null, 2)}\n`);
|
|
47
|
+
renameSync(tmp, path);
|
|
48
|
+
}
|
package/src/queue.mjs
CHANGED
|
@@ -28,14 +28,19 @@ function emptyState() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export class FlambeQueue {
|
|
31
|
-
constructor({ baseUrl, traceId, path = defaultQueuePath() }) {
|
|
31
|
+
constructor({ baseUrl, traceId, agentId, path = defaultQueuePath() }) {
|
|
32
32
|
this.baseUrl = baseUrl;
|
|
33
33
|
this.traceId = Number(traceId);
|
|
34
|
+
this.agentId = agentId;
|
|
34
35
|
this.path = path;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
matches(entry) {
|
|
38
|
-
return entry.baseUrl === this.baseUrl
|
|
39
|
+
return entry.baseUrl === this.baseUrl
|
|
40
|
+
&& entry.traceId === this.traceId
|
|
41
|
+
// Version-1 entries created before identity was recorded remain replayable.
|
|
42
|
+
// New entries may only be replayed by the worker that queued them.
|
|
43
|
+
&& (entry.agentId === undefined || entry.agentId === this.agentId);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
async read() {
|
|
@@ -70,6 +75,7 @@ export class FlambeQueue {
|
|
|
70
75
|
type,
|
|
71
76
|
baseUrl: this.baseUrl,
|
|
72
77
|
traceId: this.traceId,
|
|
78
|
+
...(this.agentId ? { agentId: this.agentId } : {}),
|
|
73
79
|
...(localId ? { localId } : {}),
|
|
74
80
|
...payload,
|
|
75
81
|
});
|
|
@@ -107,7 +113,13 @@ export class FlambeQueue {
|
|
|
107
113
|
} else if (entry.type === 'end' || entry.type === 'suspend' || entry.type === 'resume') {
|
|
108
114
|
const activityId = state.aliases[entry.activityId] ?? entry.activityId;
|
|
109
115
|
const post = { end, suspend, resume }[entry.type];
|
|
110
|
-
await post({
|
|
116
|
+
await post({
|
|
117
|
+
activityId,
|
|
118
|
+
message: entry.message,
|
|
119
|
+
timestamp: entry.timestamp,
|
|
120
|
+
force: entry.force ?? false,
|
|
121
|
+
phase: entry.phase,
|
|
122
|
+
});
|
|
111
123
|
if (entry.type === 'end') delete state.aliases[entry.activityId];
|
|
112
124
|
} else if (entry.type === 'observe') {
|
|
113
125
|
await observe(entry.input);
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Explicit transport boundary; never infer local-only intent from a URL. */
|
|
2
|
+
export function runtimeConfig(env) {
|
|
3
|
+
const mode = env.FLAMBE_RUNTIME_MODE?.trim();
|
|
4
|
+
if (!mode) return {};
|
|
5
|
+
if (!['cloud', 'local_self_contained'].includes(mode)) {
|
|
6
|
+
throw new Error('FLAMBE_RUNTIME_MODE must be cloud or local_self_contained');
|
|
7
|
+
}
|
|
8
|
+
return { runtimeMode: mode };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assertRuntimeUrl(value, mode) {
|
|
12
|
+
if (mode !== 'local_self_contained') return;
|
|
13
|
+
let url;
|
|
14
|
+
try { url = new URL(value); } catch {
|
|
15
|
+
throw new Error('Self-contained mode requires a loopback HTTP URL');
|
|
16
|
+
}
|
|
17
|
+
// Numeric addresses avoid DNS resolution changing the trust boundary.
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol)
|
|
19
|
+
|| !['127.0.0.1', '[::1]'].includes(url.hostname)
|
|
20
|
+
|| url.username || url.password) {
|
|
21
|
+
throw new Error('Self-contained mode requires 127.0.0.1 or [::1]; remote destinations are disabled');
|
|
22
|
+
}
|
|
23
|
+
}
|