@zengxingyuan/aamp-cli-bridge 0.1.7-dev.1
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 +326 -0
- package/dist/agent-bridge.d.ts +42 -0
- package/dist/agent-bridge.js +709 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/bridge.d.ts +12 -0
- package/dist/bridge.js +58 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli/init.d.ts +9 -0
- package/dist/cli/init.js +656 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/profile-maker.d.ts +1 -0
- package/dist/cli/profile-maker.js +136 -0
- package/dist/cli/profile-maker.js.map +1 -0
- package/dist/cli-agent-client.d.ts +21 -0
- package/dist/cli-agent-client.js +150 -0
- package/dist/cli-agent-client.js.map +1 -0
- package/dist/cli-profiles.d.ts +10 -0
- package/dist/cli-profiles.js +97 -0
- package/dist/cli-profiles.js.map +1 -0
- package/dist/config.d.ts +639 -0
- package/dist/config.js +95 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing.d.ts +39 -0
- package/dist/pairing.js +110 -0
- package/dist/pairing.js.map +1 -0
- package/dist/prompt-builder.d.ts +15 -0
- package/dist/prompt-builder.js +388 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/storage.d.ts +7 -0
- package/dist/storage.js +65 -0
- package/dist/storage.js.map +1 -0
- package/dist/stream-parser.d.ts +26 -0
- package/dist/stream-parser.js +155 -0
- package/dist/stream-parser.js.map +1 -0
- package/package.json +32 -0
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
5
|
+
import { AampClient } from 'aamp-sdk';
|
|
6
|
+
import * as qrcode from 'qrcode-terminal';
|
|
7
|
+
import { BUILTIN_CLI_PROFILES, getBuiltinCliProfileNames, listUserCliProfiles } from '../cli-profiles.js';
|
|
8
|
+
import { getDefaultCredentialsPath } from '../storage.js';
|
|
9
|
+
import { createPairingCode, defaultPairingFile, defaultSenderPoliciesFile, pairingUrlToWebUrl, } from '../pairing.js';
|
|
10
|
+
function ask(rl, question) {
|
|
11
|
+
if (rl.closed)
|
|
12
|
+
return Promise.resolve('');
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const onClose = () => resolve('');
|
|
15
|
+
rl.once('close', onClose);
|
|
16
|
+
try {
|
|
17
|
+
rl.question(question, (answer) => {
|
|
18
|
+
rl.off('close', onClose);
|
|
19
|
+
resolve(answer);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
rl.off('close', onClose);
|
|
24
|
+
resolve('');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function multiSelect(rl, prompt, items) {
|
|
29
|
+
if (items.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const defaultSelected = new Set(items.filter((item) => item.selected).map((item) => item.value));
|
|
32
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
33
|
+
const input = await ask(rl, `${prompt} (comma-separated, empty for current selection, "all" for all, "none" for none): `);
|
|
34
|
+
const normalized = input.trim().toLowerCase();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return items.filter((item) => defaultSelected.has(item.value)).map((item) => item.value);
|
|
37
|
+
if (normalized === 'all')
|
|
38
|
+
return items.map((item) => item.value);
|
|
39
|
+
if (normalized === 'none')
|
|
40
|
+
return [];
|
|
41
|
+
const requested = new Set(normalized.split(',').map((item) => item.trim()).filter(Boolean));
|
|
42
|
+
return items
|
|
43
|
+
.filter((item) => requested.has(item.value.toLowerCase()))
|
|
44
|
+
.map((item) => item.value);
|
|
45
|
+
}
|
|
46
|
+
return await new Promise((resolve, reject) => {
|
|
47
|
+
let cursor = 0;
|
|
48
|
+
const selected = new Set(defaultSelected);
|
|
49
|
+
let renderedLines = 0;
|
|
50
|
+
const render = () => {
|
|
51
|
+
if (renderedLines > 0) {
|
|
52
|
+
process.stdout.write(`\x1B[${renderedLines}A\x1B[0J`);
|
|
53
|
+
}
|
|
54
|
+
const lines = [
|
|
55
|
+
`${prompt} (↑/↓ to move, Space to select, Enter to confirm)`,
|
|
56
|
+
...items.map((item, index) => {
|
|
57
|
+
const pointer = index === cursor ? '>' : ' ';
|
|
58
|
+
const checkbox = selected.has(item.value) ? '[x]' : '[ ]';
|
|
59
|
+
const details = item.description ? ` ${item.description}` : '';
|
|
60
|
+
return `${pointer} ${checkbox} ${item.label}${details}`;
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
64
|
+
renderedLines = lines.length;
|
|
65
|
+
};
|
|
66
|
+
const wasRawMode = process.stdin.isRaw;
|
|
67
|
+
const cleanup = () => {
|
|
68
|
+
process.stdin.off('keypress', onKeypress);
|
|
69
|
+
process.stdin.setRawMode(wasRawMode);
|
|
70
|
+
rl.resume();
|
|
71
|
+
process.stdout.write('\n');
|
|
72
|
+
};
|
|
73
|
+
const onKeypress = (_str, key) => {
|
|
74
|
+
if (key.ctrl && key.name === 'c') {
|
|
75
|
+
cleanup();
|
|
76
|
+
reject(new Error('Selection cancelled'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
switch (key.name) {
|
|
80
|
+
case 'up':
|
|
81
|
+
case 'k':
|
|
82
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
83
|
+
render();
|
|
84
|
+
break;
|
|
85
|
+
case 'down':
|
|
86
|
+
case 'j':
|
|
87
|
+
cursor = (cursor + 1) % items.length;
|
|
88
|
+
render();
|
|
89
|
+
break;
|
|
90
|
+
case 'space': {
|
|
91
|
+
const value = items[cursor].value;
|
|
92
|
+
if (selected.has(value))
|
|
93
|
+
selected.delete(value);
|
|
94
|
+
else
|
|
95
|
+
selected.add(value);
|
|
96
|
+
render();
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'return':
|
|
100
|
+
case 'enter':
|
|
101
|
+
cleanup();
|
|
102
|
+
resolve(items.filter((item) => selected.has(item.value)).map((item) => item.value));
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
rl.pause();
|
|
109
|
+
emitKeypressEvents(process.stdin);
|
|
110
|
+
process.stdin.setRawMode(true);
|
|
111
|
+
process.stdin.resume();
|
|
112
|
+
process.stdin.on('keypress', onKeypress);
|
|
113
|
+
render();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function singleSelect(rl, prompt, items) {
|
|
117
|
+
if (items.length === 0)
|
|
118
|
+
throw new Error('No options available');
|
|
119
|
+
const defaultIndex = Math.max(0, items.findIndex((item) => item.selected));
|
|
120
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
121
|
+
const input = await ask(rl, `${prompt} (${items.map((item, index) => `${index + 1}=${item.label}`).join(', ')}, default: ${defaultIndex + 1}): `);
|
|
122
|
+
const normalized = input.trim().toLowerCase();
|
|
123
|
+
if (!normalized)
|
|
124
|
+
return items[defaultIndex].value;
|
|
125
|
+
const numericIndex = Number(normalized);
|
|
126
|
+
if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= items.length) {
|
|
127
|
+
return items[numericIndex - 1].value;
|
|
128
|
+
}
|
|
129
|
+
return items.find((item) => (item.value.toLowerCase() === normalized
|
|
130
|
+
|| item.label.toLowerCase() === normalized))?.value ?? items[defaultIndex].value;
|
|
131
|
+
}
|
|
132
|
+
return await new Promise((resolve, reject) => {
|
|
133
|
+
let cursor = defaultIndex;
|
|
134
|
+
let renderedLines = 0;
|
|
135
|
+
const render = () => {
|
|
136
|
+
if (renderedLines > 0) {
|
|
137
|
+
process.stdout.write(`\x1B[${renderedLines}A\x1B[0J`);
|
|
138
|
+
}
|
|
139
|
+
const lines = [
|
|
140
|
+
`${prompt} (↑/↓ to move, Enter to confirm)`,
|
|
141
|
+
...items.map((item, index) => {
|
|
142
|
+
const pointer = index === cursor ? '>' : ' ';
|
|
143
|
+
const details = item.description ? ` ${item.description}` : '';
|
|
144
|
+
return `${pointer} ${item.label}${details}`;
|
|
145
|
+
}),
|
|
146
|
+
];
|
|
147
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
148
|
+
renderedLines = lines.length;
|
|
149
|
+
};
|
|
150
|
+
const wasRawMode = process.stdin.isRaw;
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
process.stdin.off('keypress', onKeypress);
|
|
153
|
+
process.stdin.setRawMode(wasRawMode);
|
|
154
|
+
rl.resume();
|
|
155
|
+
process.stdout.write('\n');
|
|
156
|
+
};
|
|
157
|
+
const onKeypress = (_str, key) => {
|
|
158
|
+
if (key.ctrl && key.name === 'c') {
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(new Error('Selection cancelled'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
switch (key.name) {
|
|
164
|
+
case 'up':
|
|
165
|
+
case 'k':
|
|
166
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
167
|
+
render();
|
|
168
|
+
break;
|
|
169
|
+
case 'down':
|
|
170
|
+
case 'j':
|
|
171
|
+
cursor = (cursor + 1) % items.length;
|
|
172
|
+
render();
|
|
173
|
+
break;
|
|
174
|
+
case 'return':
|
|
175
|
+
case 'enter':
|
|
176
|
+
cleanup();
|
|
177
|
+
resolve(items[cursor].value);
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
rl.pause();
|
|
184
|
+
emitKeypressEvents(process.stdin);
|
|
185
|
+
process.stdin.setRawMode(true);
|
|
186
|
+
process.stdin.resume();
|
|
187
|
+
process.stdin.on('keypress', onKeypress);
|
|
188
|
+
render();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function parseEmailList(input) {
|
|
192
|
+
return input
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((item) => item.trim().toLowerCase())
|
|
195
|
+
.filter(Boolean);
|
|
196
|
+
}
|
|
197
|
+
function parseDispatchContextRules(raw) {
|
|
198
|
+
const trimmed = raw.trim();
|
|
199
|
+
if (!trimmed)
|
|
200
|
+
return undefined;
|
|
201
|
+
const rules = {};
|
|
202
|
+
for (const part of trimmed.split(';')) {
|
|
203
|
+
const segment = part.trim();
|
|
204
|
+
if (!segment)
|
|
205
|
+
continue;
|
|
206
|
+
const eqIdx = segment.indexOf('=');
|
|
207
|
+
if (eqIdx <= 0)
|
|
208
|
+
continue;
|
|
209
|
+
const key = segment.slice(0, eqIdx).trim().toLowerCase();
|
|
210
|
+
if (!/^[a-z0-9_-]+$/.test(key))
|
|
211
|
+
continue;
|
|
212
|
+
const values = segment
|
|
213
|
+
.slice(eqIdx + 1)
|
|
214
|
+
.split(',')
|
|
215
|
+
.map((value) => value.trim())
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
if (values.length > 0) {
|
|
218
|
+
rules[key] = values;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return Object.keys(rules).length > 0 ? rules : undefined;
|
|
222
|
+
}
|
|
223
|
+
function extractSenderPolicies(rawAgent) {
|
|
224
|
+
if (!rawAgent)
|
|
225
|
+
return undefined;
|
|
226
|
+
if (Array.isArray(rawAgent.senderPolicies) && rawAgent.senderPolicies.length > 0) {
|
|
227
|
+
return rawAgent.senderPolicies;
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(rawAgent.senderWhitelist) && rawAgent.senderWhitelist.length > 0) {
|
|
230
|
+
return rawAgent.senderWhitelist
|
|
231
|
+
.filter((sender) => typeof sender === 'string' && sender.trim().length > 0)
|
|
232
|
+
.map((sender) => ({ sender: sender.trim().toLowerCase() }));
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
function loadPreviousSenderPolicies(configPath) {
|
|
237
|
+
if (!existsSync(configPath))
|
|
238
|
+
return new Map();
|
|
239
|
+
try {
|
|
240
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
241
|
+
if (!Array.isArray(raw.agents))
|
|
242
|
+
return new Map();
|
|
243
|
+
const entries = raw.agents.flatMap((agent) => {
|
|
244
|
+
if (!agent || typeof agent !== 'object')
|
|
245
|
+
return [];
|
|
246
|
+
const record = agent;
|
|
247
|
+
const name = typeof record.name === 'string' ? record.name : '';
|
|
248
|
+
if (!name)
|
|
249
|
+
return [];
|
|
250
|
+
const policies = extractSenderPolicies(record);
|
|
251
|
+
return policies?.length ? [[name, policies]] : [];
|
|
252
|
+
});
|
|
253
|
+
return new Map(entries);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return new Map();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function loadPreviousConfig(configPath) {
|
|
260
|
+
if (!existsSync(configPath))
|
|
261
|
+
return undefined;
|
|
262
|
+
try {
|
|
263
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
264
|
+
if (!raw || !Array.isArray(raw.agents))
|
|
265
|
+
return undefined;
|
|
266
|
+
return {
|
|
267
|
+
aampHost: typeof raw.aampHost === 'string' ? raw.aampHost : 'https://meshmail.ai',
|
|
268
|
+
rejectUnauthorized: raw.rejectUnauthorized === true,
|
|
269
|
+
...(raw.profiles ? { profiles: raw.profiles } : {}),
|
|
270
|
+
agents: raw.agents,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function formatPolicySenders(policies) {
|
|
278
|
+
return policies.map((policy) => policy.sender).join(', ');
|
|
279
|
+
}
|
|
280
|
+
function getReusableSenderPolicies(name, previousPolicies, allPreviousPolicies) {
|
|
281
|
+
const reusablePolicies = [];
|
|
282
|
+
if (previousPolicies?.length) {
|
|
283
|
+
reusablePolicies.push({ label: `${name} (current)`, policies: previousPolicies });
|
|
284
|
+
}
|
|
285
|
+
reusablePolicies.push(...[...allPreviousPolicies.entries()]
|
|
286
|
+
.filter(([agentName, policies]) => agentName !== name && policies.length > 0)
|
|
287
|
+
.map(([agentName, policies]) => ({ label: agentName, policies })));
|
|
288
|
+
return reusablePolicies;
|
|
289
|
+
}
|
|
290
|
+
async function promptReusableSenderPolicies(rl, name, previousPolicies, allPreviousPolicies) {
|
|
291
|
+
const reusablePolicies = getReusableSenderPolicies(name, previousPolicies, allPreviousPolicies);
|
|
292
|
+
if (reusablePolicies.length === 0) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
if (reusablePolicies.length === 1) {
|
|
296
|
+
const [selected] = reusablePolicies;
|
|
297
|
+
console.log(` Reusing sender policies from ${selected.label} (${formatPolicySenders(selected.policies)})`);
|
|
298
|
+
return selected.policies;
|
|
299
|
+
}
|
|
300
|
+
const selectedIndex = await singleSelect(rl, `? Reuse which sender policies for ${name}?`, reusablePolicies.map(({ label, policies }, index) => ({
|
|
301
|
+
value: String(index),
|
|
302
|
+
label,
|
|
303
|
+
description: `(${formatPolicySenders(policies)})`,
|
|
304
|
+
selected: index === 0,
|
|
305
|
+
})));
|
|
306
|
+
return reusablePolicies[Number(selectedIndex)].policies;
|
|
307
|
+
}
|
|
308
|
+
async function promptManualSenderPolicies(rl, name) {
|
|
309
|
+
const sendersInput = await ask(rl, `? Allowed sender emails for ${name} (comma-separated): `);
|
|
310
|
+
const senders = parseEmailList(sendersInput);
|
|
311
|
+
if (senders.length === 0) {
|
|
312
|
+
console.log(` Warning: no valid sender entries provided; ${name} will reject all senders until paired or configured.`);
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const senderPolicies = [];
|
|
316
|
+
for (const sender of senders) {
|
|
317
|
+
const rulesInput = await ask(rl, `? Dispatch context rules for ${sender} (optional, format: project_key=proj1,proj2; user_key=alice): `);
|
|
318
|
+
const dispatchContextRules = parseDispatchContextRules(rulesInput);
|
|
319
|
+
senderPolicies.push({
|
|
320
|
+
sender,
|
|
321
|
+
...(dispatchContextRules ? { dispatchContextRules } : {}),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return senderPolicies;
|
|
325
|
+
}
|
|
326
|
+
async function promptSenderPolicies(rl, name, method, previousPolicies, allPreviousPolicies) {
|
|
327
|
+
if (method === 'reuse-sender-policy') {
|
|
328
|
+
return promptReusableSenderPolicies(rl, name, previousPolicies, allPreviousPolicies);
|
|
329
|
+
}
|
|
330
|
+
return promptManualSenderPolicies(rl, name);
|
|
331
|
+
}
|
|
332
|
+
async function promptConnectionSetupMethod(rl, name, canReuseSenderPolicy) {
|
|
333
|
+
return singleSelect(rl, `? How should ${name} authorize senders?`, [
|
|
334
|
+
{
|
|
335
|
+
value: 'pairing-code',
|
|
336
|
+
label: 'Pair with QR code',
|
|
337
|
+
description: '(recommended)',
|
|
338
|
+
selected: true,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
value: 'manual-sender-policy',
|
|
342
|
+
label: 'Manually enter sender policy',
|
|
343
|
+
},
|
|
344
|
+
...(canReuseSenderPolicy ? [{
|
|
345
|
+
value: 'reuse-sender-policy',
|
|
346
|
+
label: 'Reuse existing sender policy',
|
|
347
|
+
}] : []),
|
|
348
|
+
{
|
|
349
|
+
value: 'later',
|
|
350
|
+
label: 'Configure later',
|
|
351
|
+
},
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
function renderCommandForDetection(command, profileName) {
|
|
355
|
+
const rendered = command.replace(/\{\{\s*agentName\s*\}\}/g, profileName);
|
|
356
|
+
if (/\{\{/.test(rendered))
|
|
357
|
+
return null;
|
|
358
|
+
if (/\s/.test(rendered.trim()))
|
|
359
|
+
return null;
|
|
360
|
+
return rendered.trim() || null;
|
|
361
|
+
}
|
|
362
|
+
function detectCommand(command) {
|
|
363
|
+
try {
|
|
364
|
+
execFileSync('which', [command], { stdio: 'pipe' });
|
|
365
|
+
try {
|
|
366
|
+
return execFileSync(command, ['--version'], { stdio: 'pipe' }).toString().trim().split('\n')[0] || 'installed';
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return 'installed';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function profileLabel(profileRef) {
|
|
377
|
+
if (typeof profileRef === 'string')
|
|
378
|
+
return profileRef;
|
|
379
|
+
return profileRef.name ?? 'inline';
|
|
380
|
+
}
|
|
381
|
+
function stableStringify(value) {
|
|
382
|
+
if (Array.isArray(value)) {
|
|
383
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
384
|
+
}
|
|
385
|
+
if (value && typeof value === 'object') {
|
|
386
|
+
return `{${Object.entries(value)
|
|
387
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
388
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
389
|
+
.join(',')}}`;
|
|
390
|
+
}
|
|
391
|
+
return JSON.stringify(value);
|
|
392
|
+
}
|
|
393
|
+
function profilesEquivalent(left, right) {
|
|
394
|
+
if (!left || !right)
|
|
395
|
+
return false;
|
|
396
|
+
return stableStringify(left) === stableStringify(right);
|
|
397
|
+
}
|
|
398
|
+
function resolveProfileForExistingAgent(agent, customProfiles) {
|
|
399
|
+
if (typeof agent.cliProfile !== 'string')
|
|
400
|
+
return agent.cliProfile;
|
|
401
|
+
return customProfiles?.[agent.cliProfile]
|
|
402
|
+
?? BUILTIN_CLI_PROFILES[agent.cliProfile]
|
|
403
|
+
?? listUserCliProfiles().find((item) => item.name === agent.cliProfile)?.profile;
|
|
404
|
+
}
|
|
405
|
+
function collectProfileCandidates(previousConfig) {
|
|
406
|
+
const profiles = new Map();
|
|
407
|
+
for (const name of getBuiltinCliProfileNames()) {
|
|
408
|
+
profiles.set(name, {
|
|
409
|
+
name,
|
|
410
|
+
cliProfile: name,
|
|
411
|
+
profile: BUILTIN_CLI_PROFILES[name],
|
|
412
|
+
source: 'built-in',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
for (const [name, profile] of Object.entries(previousConfig?.profiles ?? {})) {
|
|
416
|
+
profiles.set(name, {
|
|
417
|
+
name,
|
|
418
|
+
cliProfile: name,
|
|
419
|
+
profile,
|
|
420
|
+
source: 'config',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
for (const { name, profile } of listUserCliProfiles()) {
|
|
424
|
+
const existing = profiles.get(name);
|
|
425
|
+
if (existing?.source === 'built-in' && profilesEquivalent(existing.profile, profile)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
profiles.set(name, {
|
|
429
|
+
name,
|
|
430
|
+
cliProfile: name,
|
|
431
|
+
profile,
|
|
432
|
+
source: 'user',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const existingAgents = new Map((previousConfig?.agents ?? []).map((agent) => [agent.name, agent]));
|
|
436
|
+
for (const agent of existingAgents.values()) {
|
|
437
|
+
const existing = profiles.get(agent.name);
|
|
438
|
+
const profile = resolveProfileForExistingAgent(agent, previousConfig?.profiles);
|
|
439
|
+
if (existing) {
|
|
440
|
+
profiles.set(agent.name, {
|
|
441
|
+
...existing,
|
|
442
|
+
existingAgent: agent,
|
|
443
|
+
cliProfile: agent.cliProfile,
|
|
444
|
+
profile: profile ?? existing.profile,
|
|
445
|
+
});
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
profiles.set(agent.name, {
|
|
449
|
+
name: agent.name,
|
|
450
|
+
cliProfile: agent.cliProfile,
|
|
451
|
+
...(profile ? { profile } : {}),
|
|
452
|
+
source: 'configured',
|
|
453
|
+
existingAgent: agent,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return [...profiles.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
457
|
+
}
|
|
458
|
+
function detectProfileCandidate(candidate) {
|
|
459
|
+
const command = candidate.profile
|
|
460
|
+
? renderCommandForDetection(candidate.profile.command, candidate.name)
|
|
461
|
+
: null;
|
|
462
|
+
if (!command)
|
|
463
|
+
return candidate;
|
|
464
|
+
const version = detectCommand(command);
|
|
465
|
+
return {
|
|
466
|
+
...candidate,
|
|
467
|
+
command,
|
|
468
|
+
...(version ? { version } : {}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function formatProfileScanLine(candidate) {
|
|
472
|
+
const label = `${candidate.name} [${candidate.source}]`;
|
|
473
|
+
if (candidate.version) {
|
|
474
|
+
return ` + ${label.padEnd(24)} ${candidate.command} (${candidate.version})`;
|
|
475
|
+
}
|
|
476
|
+
if (candidate.existingAgent) {
|
|
477
|
+
const profile = profileLabel(candidate.cliProfile);
|
|
478
|
+
return ` * ${label.padEnd(24)} ${profile} (already configured, not detected)`;
|
|
479
|
+
}
|
|
480
|
+
const command = candidate.command ?? candidate.profile?.command ?? profileLabel(candidate.cliProfile);
|
|
481
|
+
return ` - ${label.padEnd(24)} ${command} (not detected)`;
|
|
482
|
+
}
|
|
483
|
+
function renderScanProgress(index, total, candidate) {
|
|
484
|
+
const label = `${candidate.name} [${candidate.source}]`;
|
|
485
|
+
process.stdout.write(` scanning ${index}/${total}: ${label}...\r`);
|
|
486
|
+
}
|
|
487
|
+
function clearScanProgress() {
|
|
488
|
+
process.stdout.write('\x1B[2K\r');
|
|
489
|
+
}
|
|
490
|
+
function renderQrFallback(value) {
|
|
491
|
+
console.log(` Pairing URL: ${value}`);
|
|
492
|
+
}
|
|
493
|
+
function renderTerminalQr(value) {
|
|
494
|
+
const qrModule = (qrcode.generate ? qrcode : qrcode.default);
|
|
495
|
+
if (!qrModule?.generate) {
|
|
496
|
+
renderQrFallback(value);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
console.log(' Scan this QR code with AAMP App:');
|
|
501
|
+
qrModule.generate(pairingUrlToWebUrl(value), { small: true }, (qr) => console.log(qr));
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
renderQrFallback(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export function renderPairingCode(name, mailbox, pairingFile) {
|
|
508
|
+
const pairing = createPairingCode({ mailbox, file: pairingFile });
|
|
509
|
+
console.log(`\n Pair ${name} with AAMP App (expires ${pairing.expiresAt})`);
|
|
510
|
+
renderTerminalQr(pairing.connectUrl);
|
|
511
|
+
console.log(` Pairing URL: ${pairing.connectUrl}`);
|
|
512
|
+
}
|
|
513
|
+
export async function runInit(configPath, opts = {}) {
|
|
514
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
515
|
+
const previousConfig = loadPreviousConfig(configPath);
|
|
516
|
+
const previousAgents = new Map((previousConfig?.agents ?? []).map((agent) => [agent.name, agent]));
|
|
517
|
+
console.log('\nAAMP CLI Bridge Setup\n');
|
|
518
|
+
const defaultAampHost = previousConfig?.aampHost ?? 'https://meshmail.ai';
|
|
519
|
+
const aampHostInput = opts.aampHost
|
|
520
|
+
? opts.aampHost
|
|
521
|
+
: (await ask(rl, `? AAMP Service URL (default: ${defaultAampHost}): `)).trim();
|
|
522
|
+
const aampHost = aampHostInput || defaultAampHost;
|
|
523
|
+
try {
|
|
524
|
+
const res = await fetch(`${aampHost}/health`);
|
|
525
|
+
if (res.ok) {
|
|
526
|
+
const data = await res.json();
|
|
527
|
+
console.log(` Connected (${data.service})\n`);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.log(` Warning: Server responded with ${res.status}\n`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
console.log(` Warning: Could not reach ${aampHost} -- continuing anyway\n`);
|
|
535
|
+
}
|
|
536
|
+
console.log(opts.agent ? `? Scanning CLI profile: ${opts.agent}` : '? Scanning CLI profiles...');
|
|
537
|
+
const detected = [];
|
|
538
|
+
const candidates = collectProfileCandidates(previousConfig)
|
|
539
|
+
.filter((candidate) => !opts.agent || candidate.name === opts.agent);
|
|
540
|
+
if (opts.agent && candidates.length === 0) {
|
|
541
|
+
rl.close();
|
|
542
|
+
throw new Error(`Unknown CLI profile "${opts.agent}". Built-in profiles: ${getBuiltinCliProfileNames().join(', ')}`);
|
|
543
|
+
}
|
|
544
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
545
|
+
renderScanProgress(index + 1, candidates.length, candidate);
|
|
546
|
+
const scanned = detectProfileCandidate(candidate);
|
|
547
|
+
clearScanProgress();
|
|
548
|
+
console.log(formatProfileScanLine(scanned));
|
|
549
|
+
if (scanned.version || scanned.existingAgent)
|
|
550
|
+
detected.push(scanned);
|
|
551
|
+
}
|
|
552
|
+
console.log();
|
|
553
|
+
const selected = opts.agent
|
|
554
|
+
? detected.map((candidate) => candidate.name)
|
|
555
|
+
: await multiSelect(rl, '? Select CLI profiles to bridge', detected.map((candidate) => ({
|
|
556
|
+
value: candidate.name,
|
|
557
|
+
label: `${candidate.name} [${candidate.source}]`,
|
|
558
|
+
description: candidate.command
|
|
559
|
+
? `${candidate.command} (${candidate.version ?? 'detected'})`
|
|
560
|
+
: `${profileLabel(candidate.cliProfile)}${candidate.existingAgent ? ' (already configured)' : ''}`,
|
|
561
|
+
selected: Boolean(candidate.existingAgent),
|
|
562
|
+
})));
|
|
563
|
+
const selectedSet = new Set(selected);
|
|
564
|
+
const agents = [];
|
|
565
|
+
const previousSenderPolicies = loadPreviousSenderPolicies(configPath);
|
|
566
|
+
for (const candidate of detected.filter((item) => selectedSet.has(item.name))) {
|
|
567
|
+
const { name } = candidate;
|
|
568
|
+
const previousAgent = previousAgents.get(name);
|
|
569
|
+
const credFile = previousAgent?.credentialsFile ?? getDefaultCredentialsPath(name);
|
|
570
|
+
const pairingFile = previousAgent?.pairingFile ?? defaultPairingFile(name);
|
|
571
|
+
const senderPoliciesFile = previousAgent?.senderPoliciesFile ?? defaultSenderPoliciesFile(name);
|
|
572
|
+
const previousPolicies = previousSenderPolicies.get(name);
|
|
573
|
+
const canReuseSenderPolicy = getReusableSenderPolicies(name, previousPolicies, previousSenderPolicies).length > 0;
|
|
574
|
+
const connectionSetup = opts.connectionSetup
|
|
575
|
+
?? await promptConnectionSetupMethod(rl, name, canReuseSenderPolicy);
|
|
576
|
+
if (connectionSetup === 'reuse-sender-policy' && !canReuseSenderPolicy) {
|
|
577
|
+
rl.close();
|
|
578
|
+
throw new Error(`Cannot reuse sender policy for ${name}: no existing sender policies found`);
|
|
579
|
+
}
|
|
580
|
+
const senderPolicies = connectionSetup === 'manual-sender-policy' || connectionSetup === 'reuse-sender-policy'
|
|
581
|
+
? await promptSenderPolicies(rl, name, connectionSetup, previousPolicies, previousSenderPolicies)
|
|
582
|
+
: undefined;
|
|
583
|
+
const baseAgent = {
|
|
584
|
+
...(previousAgent ?? {
|
|
585
|
+
name,
|
|
586
|
+
cliProfile: candidate.cliProfile,
|
|
587
|
+
slug: `${name}-cli-bridge`,
|
|
588
|
+
description: `${name} via CLI bridge`,
|
|
589
|
+
}),
|
|
590
|
+
name,
|
|
591
|
+
cliProfile: previousAgent?.cliProfile ?? candidate.cliProfile,
|
|
592
|
+
slug: previousAgent?.slug ?? `${name}-cli-bridge`,
|
|
593
|
+
credentialsFile: credFile,
|
|
594
|
+
pairingFile,
|
|
595
|
+
senderPoliciesFile,
|
|
596
|
+
senderPolicies,
|
|
597
|
+
};
|
|
598
|
+
delete baseAgent.senderWhitelist;
|
|
599
|
+
if (existsSync(credFile)) {
|
|
600
|
+
const saved = JSON.parse(readFileSync(credFile, 'utf-8'));
|
|
601
|
+
if (connectionSetup === 'pairing-code' && saved.email) {
|
|
602
|
+
renderPairingCode(name, saved.email, pairingFile);
|
|
603
|
+
}
|
|
604
|
+
else if (connectionSetup === 'later') {
|
|
605
|
+
console.log(` Sender authorization for ${name} was left for later; task.dispatch will be rejected until paired or configured.`);
|
|
606
|
+
}
|
|
607
|
+
console.log(` + ${name} -> using existing credentials (${credFile})`);
|
|
608
|
+
agents.push(baseAgent);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const creds = await AampClient.registerMailbox({
|
|
613
|
+
aampHost,
|
|
614
|
+
slug: `${name}-cli-bridge`,
|
|
615
|
+
description: `${name} via CLI bridge`,
|
|
616
|
+
});
|
|
617
|
+
mkdirSync(dirname(credFile), { recursive: true });
|
|
618
|
+
writeFileSync(credFile, JSON.stringify({
|
|
619
|
+
email: creds.email,
|
|
620
|
+
mailboxToken: creds.mailboxToken,
|
|
621
|
+
smtpPassword: creds.smtpPassword,
|
|
622
|
+
}, null, 2));
|
|
623
|
+
if (connectionSetup === 'pairing-code') {
|
|
624
|
+
renderPairingCode(name, creds.email, pairingFile);
|
|
625
|
+
}
|
|
626
|
+
else if (connectionSetup === 'later') {
|
|
627
|
+
console.log(` Sender authorization for ${name} was left for later; task.dispatch will be rejected until paired or configured.`);
|
|
628
|
+
}
|
|
629
|
+
console.log(` + ${name} -> ${creds.email}`);
|
|
630
|
+
agents.push({
|
|
631
|
+
...baseAgent,
|
|
632
|
+
credentialsFile: credFile,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
console.log(` x ${name} -> registration failed: ${err.message}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (agents.length === 0) {
|
|
640
|
+
console.log('No agents selected. Use profile-maker for custom CLI agents, then edit the config.');
|
|
641
|
+
rl.close();
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
const config = {
|
|
645
|
+
...(previousConfig ?? {}),
|
|
646
|
+
aampHost,
|
|
647
|
+
rejectUnauthorized: previousConfig?.rejectUnauthorized ?? false,
|
|
648
|
+
agents,
|
|
649
|
+
};
|
|
650
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
651
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
652
|
+
console.log(`\nConfig written to ${configPath}`);
|
|
653
|
+
rl.close();
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
//# sourceMappingURL=init.js.map
|