@thinkingai/ae-cli 6.0.47 → 6.0.48-workbuddy
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 +93 -36
- package/README.zh.md +93 -36
- package/assets/workbuddy-host-page/thinkingai.png +0 -0
- package/assets/workbuddy-host-page/workbuddy.png +0 -0
- package/bin/ae-cli-workbuddy.js +7 -0
- package/dist/auth-HJG2L6DR.js +910 -0
- package/dist/{capability-RUSV77CO.js → capability-JHYVQNQ4.js} +47 -34
- package/dist/chunk-2BDQB42E.js +15 -0
- package/dist/{chunk-GJXRF7Y6.js → chunk-6MXFJVNP.js} +47 -78
- package/dist/chunk-6ZIQV5GW.js +26 -0
- package/dist/{chunk-KZ6ZHF5J.js → chunk-ARTIHWVD.js} +27 -25
- package/dist/{chunk-6QK7SYYY.js → chunk-DU5MZQFQ.js} +29 -37
- package/dist/{chunk-BVDST5YT.js → chunk-JFOGWBAT.js} +8 -8
- package/dist/{chunk-JYMGHGCE.js → chunk-KSQGGXGA.js} +24 -13
- package/dist/{chunk-FR2EDAQM.js → chunk-NXRKDG6E.js} +89 -3
- package/dist/{chunk-NSIVXKJH.js → chunk-QSO4SA6C.js} +5 -5
- package/dist/{chunk-QL6R6TSK.js → chunk-QYJ252CT.js} +6 -5
- package/dist/{chunk-B5HMQAOM.js → chunk-UL6SJDYE.js} +1 -1
- package/dist/chunk-Y3S2KNNR.js +623 -0
- package/dist/{chunk-KEWVSO64.js → chunk-ZCM47ROU.js} +5 -16
- package/dist/{community-report-client-6OL6GBQW.js → community-report-client-QE4RC2XG.js} +2 -4
- package/dist/{config-XNMACCKX.js → config-XMRP5NDA.js} +8 -9
- package/dist/{data-integration-4NZGZ45E.js → data-integration-SGRITHJS.js} +19 -18
- package/dist/index.js +53 -80
- package/dist/{local-data-upload-client-EFHDLXFG.js → local-data-upload-client-LMVS7YAR.js} +2 -4
- package/dist/{memory-HBJQ2GC5.js → memory-5ZC73XBC.js} +4 -6
- package/dist/{metadata-PGDJE5RQ.js → metadata-LBX6O24P.js} +7 -9
- package/dist/{model-BHLJ2QMT.js → model-CRDIVYN5.js} +4 -6
- package/dist/{personal-semantic-preference-E5NZ56DN.js → personal-semantic-preference-7FVUTJZF.js} +6 -8
- package/dist/{sync-MUWPRM5I.js → sync-VVNDHU7R.js} +9 -11
- package/dist/{te-agent-WQZTMEK4.js → te-agent-4T5TFHD6.js} +393 -90
- package/dist/{te-analysis-YWCSSLYL.js → te-analysis-TPYFOIFD.js} +97 -31
- package/dist/{te-community-BNE6KREL.js → te-community-PWOTHSEC.js} +9 -32
- package/dist/{te-dataops-TXLKM2W6.js → te-dataops-ZJV5QVJU.js} +19 -20
- package/dist/{te-engage-VEW4CJHK.js → te-engage-WVHBZAXH.js} +11 -13
- package/dist/{te-kb-BUD6N3NI.js → te-kb-PNM2ZKBP.js} +595 -85
- package/dist/{te-system-44LZJDTQ.js → te-system-MRCMLAWW.js} +6 -8
- package/dist/{te-team-XMP47YXE.js → te-team-Y36TG6AQ.js} +6 -8
- package/dist/{update-UODFOZ4P.js → update-UNKCZ5U5.js} +7 -8
- package/package.json +15 -10
- package/skills/ae-agent/SKILL.md +30 -13
- package/skills/ae-agent/references/agent-distribution.md +94 -0
- package/skills/ae-agent/references/approval-request.md +4 -0
- package/skills/ae-agent/references/command_index.md +9 -2
- package/skills/ae-agent/references/create-automation.md +20 -2
- package/skills/ae-agent/references/get-agent-context.md +70 -0
- package/skills/ae-agent/references/list-automations.md +18 -3
- package/skills/ae-agent/references/list-mcp-credentials.md +1 -1
- package/skills/ae-agent/references/mcp-token.md +3 -3
- package/skills/ae-agent/references/set-mcp-credential.md +0 -1
- package/skills/ae-agent/references/update-automation.md +18 -0
- package/skills/ae-analysis/SKILL.md +11 -2
- package/skills/ae-analysis/references/adhoc_run.md +2 -0
- package/skills/ae-analysis/references/ai_models.md +23 -3
- package/skills/ae-analysis/references/analysis_gateway_assets.md +2 -2
- package/skills/ae-analysis/references/command_index.md +7 -7
- package/skills/ae-analysis/references/cross_source_config.md +84 -0
- package/skills/ae-analysis/references/dashboard_update.md +1 -1
- package/skills/ae-analysis/references/user_tag_create.md +23 -1
- package/skills/ae-analysis/references/user_tag_models.md +9 -1
- package/skills/ae-analysis/references/user_tag_update.md +14 -2
- package/skills/ae-data-integration/SKILL.md +2 -1
- package/skills/ae-data-integration/references/dimension-routing.md +36 -0
- package/skills/ae-data-integration/references/local-analysis.md +2 -0
- package/skills/ae-data-integration/references/source-inspect.md +1 -1
- package/skills/ae-data-integration/references/tracking-plan.md +1 -1
- package/skills/ae-data-integration/references/transform.md +1 -1
- package/skills/ae-data-integration/references/ue-mapping.md +5 -2
- package/skills/ae-data-integration/references/ue-routing.md +17 -1
- package/skills/ae-generate-tracking-plan/SKILL.md +25 -13
- package/skills/ae-generate-tracking-plan/references/business-dimension-mapping.md +1 -1
- package/skills/ae-kb/SKILL.md +107 -36
- package/skills/ae-kb/references/query-workflow.md +59 -29
- package/skills/ae-kb/references/versions.md +46 -0
- package/skills/ae-system/SKILL.md +1 -1
- package/skills/ae-use-agent/SKILL.md +42 -0
- package/skills/ae-use-agent/references/local-agent.md +114 -0
- package/dist/auth-QPEVVBKT.js +0 -465
- package/dist/auth-UMJAA72O.js +0 -15
- package/dist/chunk-533JK3YX.js +0 -72
- package/dist/chunk-O6K2ODUE.js +0 -318
- package/dist/chunk-RWXIXOZG.js +0 -326
- package/dist/client-N7NGZUNI.js +0 -204
- package/skills/ae-agent/references/auto-provision-mcp-credentials.md +0 -57
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
MultiselectCancelled,
|
|
4
|
+
promptSingleSelect
|
|
5
|
+
} from "./chunk-7K24F7T2.js";
|
|
6
|
+
import {
|
|
7
|
+
missingAeHostGuidance,
|
|
8
|
+
missingAeHostHint
|
|
9
|
+
} from "./chunk-6ZIQV5GW.js";
|
|
10
|
+
import {
|
|
11
|
+
getLocalCliPackageInfo
|
|
12
|
+
} from "./chunk-E2JKXMVO.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveHost
|
|
15
|
+
} from "./chunk-2BDQB42E.js";
|
|
16
|
+
import {
|
|
17
|
+
printError,
|
|
18
|
+
printOutput,
|
|
19
|
+
runHostCompatCheck
|
|
20
|
+
} from "./chunk-ZCM47ROU.js";
|
|
21
|
+
import {
|
|
22
|
+
CliTokenValidationUnavailableError,
|
|
23
|
+
SecureStoreAuthError,
|
|
24
|
+
activateCredential,
|
|
25
|
+
clearAllCredentials,
|
|
26
|
+
clearCliTokenCache,
|
|
27
|
+
getActiveCredential,
|
|
28
|
+
listCredentials,
|
|
29
|
+
mintCliToken,
|
|
30
|
+
removeActiveCredential,
|
|
31
|
+
saveCredential,
|
|
32
|
+
updateCredentialMetadata,
|
|
33
|
+
validateCliTokenOnServer
|
|
34
|
+
} from "./chunk-Y3S2KNNR.js";
|
|
35
|
+
import {
|
|
36
|
+
getFallbackCliToken,
|
|
37
|
+
listHosts,
|
|
38
|
+
loadConfig,
|
|
39
|
+
normalizeUrl,
|
|
40
|
+
saveConfig
|
|
41
|
+
} from "./chunk-ARTIHWVD.js";
|
|
42
|
+
import {
|
|
43
|
+
PermissionError,
|
|
44
|
+
logger
|
|
45
|
+
} from "./chunk-NXRKDG6E.js";
|
|
46
|
+
|
|
47
|
+
// src/core/device-auth.ts
|
|
48
|
+
import { execFileSync } from "child_process";
|
|
49
|
+
var DeviceFlowUnsupportedError = class extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super("This AE server does not support device code login (it may be an older server version).");
|
|
52
|
+
this.name = "DeviceFlowUnsupportedError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function looksLikeMissingEndpoint(status, contentType, body) {
|
|
56
|
+
if (status === 404) return true;
|
|
57
|
+
if (contentType && contentType.toLowerCase().includes("text/html")) return true;
|
|
58
|
+
const head = body.trimStart().toLowerCase();
|
|
59
|
+
return head.startsWith("<!doctype html") || head.startsWith("<html");
|
|
60
|
+
}
|
|
61
|
+
function openBrowser(url, _execFile) {
|
|
62
|
+
let parsedUrl;
|
|
63
|
+
try {
|
|
64
|
+
parsedUrl = new URL(url);
|
|
65
|
+
} catch {
|
|
66
|
+
logger.warn(`openBrowser: invalid URL, not opening`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
70
|
+
logger.warn(`openBrowser: unsafe protocol "${parsedUrl.protocol}", not opening`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const safeLogUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;
|
|
74
|
+
const exec = _execFile ?? execFileSync;
|
|
75
|
+
try {
|
|
76
|
+
if (process.platform === "darwin") {
|
|
77
|
+
exec("open", [url], { timeout: 5e3 });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (process.platform === "win32") {
|
|
81
|
+
exec("cmd", ["/c", "start", "", url], { timeout: 5e3 });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
exec("xdg-open", [url], { timeout: 5e3 });
|
|
85
|
+
return true;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
logger.warn(`openBrowser: failed to open ${safeLogUrl}: ${e.message}`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function authorizeDevice(host) {
|
|
92
|
+
const base = host.replace(/\/+$/, "");
|
|
93
|
+
const url = `${base}/api/auth/device/authorize`;
|
|
94
|
+
let resp;
|
|
95
|
+
try {
|
|
96
|
+
resp = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" }
|
|
99
|
+
});
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`Device authorize request failed: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
const contentType = resp.headers.get("content-type");
|
|
104
|
+
const text = await resp.text().catch(() => "");
|
|
105
|
+
if (looksLikeMissingEndpoint(resp.status, contentType, text)) {
|
|
106
|
+
throw new DeviceFlowUnsupportedError();
|
|
107
|
+
}
|
|
108
|
+
if (!resp.ok) {
|
|
109
|
+
throw new Error(`Device authorize returned HTTP ${resp.status}: ${text}`);
|
|
110
|
+
}
|
|
111
|
+
let body;
|
|
112
|
+
try {
|
|
113
|
+
body = JSON.parse(text);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error("Device authorize returned invalid JSON");
|
|
116
|
+
}
|
|
117
|
+
if (!body.device_code || !body.user_code) {
|
|
118
|
+
throw new Error("Device authorize response missing required fields");
|
|
119
|
+
}
|
|
120
|
+
return body;
|
|
121
|
+
}
|
|
122
|
+
async function pollDeviceToken(host, deviceCode) {
|
|
123
|
+
const base = host.replace(/\/+$/, "");
|
|
124
|
+
const url = `${base}/api/auth/device/token`;
|
|
125
|
+
let resp;
|
|
126
|
+
try {
|
|
127
|
+
resp = await fetch(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
131
|
+
});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return { status: "error", message: `Network error: ${e.message}` };
|
|
134
|
+
}
|
|
135
|
+
if (resp.status === 200) {
|
|
136
|
+
let body;
|
|
137
|
+
try {
|
|
138
|
+
body = await resp.json();
|
|
139
|
+
} catch {
|
|
140
|
+
return { status: "error", message: "Token response is not valid JSON" };
|
|
141
|
+
}
|
|
142
|
+
if (!body.access_token) {
|
|
143
|
+
return { status: "error", message: "Token response missing access_token" };
|
|
144
|
+
}
|
|
145
|
+
return { status: "approved", tokens: body };
|
|
146
|
+
}
|
|
147
|
+
if (resp.status === 400) {
|
|
148
|
+
let errorBody;
|
|
149
|
+
try {
|
|
150
|
+
errorBody = await resp.json();
|
|
151
|
+
} catch {
|
|
152
|
+
errorBody = {};
|
|
153
|
+
}
|
|
154
|
+
const err = errorBody.error;
|
|
155
|
+
if (err === "authorization_pending") return { status: "pending" };
|
|
156
|
+
if (err === "slow_down") return { status: "slow_down" };
|
|
157
|
+
if (err === "expired_token") return { status: "expired" };
|
|
158
|
+
return { status: "error", message: `Unknown error: ${err ?? "unknown"}` };
|
|
159
|
+
}
|
|
160
|
+
if (resp.status === 403) {
|
|
161
|
+
let errorBody = {};
|
|
162
|
+
try {
|
|
163
|
+
errorBody = await resp.json();
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
status: "denied",
|
|
168
|
+
code: errorBody.code ?? "CLI_ACCESS_DISABLED",
|
|
169
|
+
message: errorBody.message ?? errorBody.error ?? "CLI access is disabled for this account.",
|
|
170
|
+
hint: errorBody.hint
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const contentType = resp.headers.get("content-type");
|
|
174
|
+
const text = await resp.text().catch(() => "");
|
|
175
|
+
if (looksLikeMissingEndpoint(resp.status, contentType, text)) {
|
|
176
|
+
return { status: "unsupported" };
|
|
177
|
+
}
|
|
178
|
+
return { status: "error", message: `Unexpected HTTP ${resp.status} from token endpoint` };
|
|
179
|
+
}
|
|
180
|
+
function buildVerificationUrl(host, userCode) {
|
|
181
|
+
return `${host.replace(/\/+$/, "")}/device-activate?user_code=${encodeURIComponent(userCode)}`;
|
|
182
|
+
}
|
|
183
|
+
async function pollDeviceFlow(host, deviceCode, options = {}, onStatus) {
|
|
184
|
+
const emit = (msg) => {
|
|
185
|
+
if (onStatus) onStatus(msg);
|
|
186
|
+
};
|
|
187
|
+
let intervalMs = options.intervalMs ?? 5e3;
|
|
188
|
+
const expiresIn = options.expiresIn ?? 300;
|
|
189
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
190
|
+
let pollCount = 0;
|
|
191
|
+
while (Date.now() < deadline) {
|
|
192
|
+
await sleep(intervalMs);
|
|
193
|
+
pollCount++;
|
|
194
|
+
const result = await pollDeviceToken(host, deviceCode);
|
|
195
|
+
if (result.status === "approved") {
|
|
196
|
+
emit(`Authorization successful! (polled ${pollCount} time(s))`);
|
|
197
|
+
return result.tokens;
|
|
198
|
+
}
|
|
199
|
+
if (result.status === "pending") {
|
|
200
|
+
if (pollCount % 5 === 0) {
|
|
201
|
+
const remaining = Math.max(0, Math.round((deadline - Date.now()) / 1e3));
|
|
202
|
+
emit(`Waiting for authorization... (expires in ${remaining}s)`);
|
|
203
|
+
}
|
|
204
|
+
logger.info(`device-auth: poll #${pollCount} pending`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (result.status === "slow_down") {
|
|
208
|
+
intervalMs += 5e3;
|
|
209
|
+
logger.info(`device-auth: slow_down received, new interval=${intervalMs}ms`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (result.status === "expired") {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Device code has expired. Please run ae-cli auth login again to restart the authorization flow.`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (result.status === "denied") {
|
|
218
|
+
throw new PermissionError(result.message, result.code, result.hint);
|
|
219
|
+
}
|
|
220
|
+
if (result.status === "unsupported") {
|
|
221
|
+
throw new DeviceFlowUnsupportedError();
|
|
222
|
+
}
|
|
223
|
+
if (result.status === "error") {
|
|
224
|
+
logger.warn(`device-auth: poll error: ${result.message}`);
|
|
225
|
+
emit(`Poll error: ${result.message}`);
|
|
226
|
+
if (result.message.includes("HTTP 401") || result.message.includes("HTTP 403")) {
|
|
227
|
+
throw new Error(`Authorization denied (${result.message}). Please check your configuration or run ae-cli auth login again.`);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Authorization timed out (not completed within ${expiresIn}s). Please run ae-cli auth login again.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
async function runDeviceFlow(host, options = {}, onStatus) {
|
|
237
|
+
const emit = (msg) => {
|
|
238
|
+
if (onStatus) onStatus(msg);
|
|
239
|
+
};
|
|
240
|
+
emit("Requesting authorization code from server...");
|
|
241
|
+
const auth = await authorizeDevice(host);
|
|
242
|
+
const userCode = auth.user_code;
|
|
243
|
+
const verifyUrl = buildVerificationUrl(host, userCode);
|
|
244
|
+
const expiresIn = auth.expires_in ?? 300;
|
|
245
|
+
emit("");
|
|
246
|
+
emit(`Verification code: ${userCode}`);
|
|
247
|
+
emit(`Open the following link in your browser to complete authorization:`);
|
|
248
|
+
emit(` ${verifyUrl}`);
|
|
249
|
+
emit("");
|
|
250
|
+
if (!options.noBrowser) {
|
|
251
|
+
const opened = openBrowser(verifyUrl);
|
|
252
|
+
if (opened) {
|
|
253
|
+
emit("(Attempted to open the link in your browser)");
|
|
254
|
+
} else {
|
|
255
|
+
emit("(Could not open browser automatically; please copy the link above manually)");
|
|
256
|
+
}
|
|
257
|
+
emit("");
|
|
258
|
+
}
|
|
259
|
+
return pollDeviceFlow(host, auth.device_code, { intervalMs: (auth.interval ?? 5) * 1e3, expiresIn }, onStatus);
|
|
260
|
+
}
|
|
261
|
+
function sleep(ms) {
|
|
262
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/integrations/workbuddy/host-page.ts
|
|
266
|
+
import { randomBytes } from "crypto";
|
|
267
|
+
import fs from "fs";
|
|
268
|
+
import { createServer } from "http";
|
|
269
|
+
import { fileURLToPath } from "url";
|
|
270
|
+
var LOOPBACK_ADDRESS = "127.0.0.1";
|
|
271
|
+
var MAX_REQUEST_BYTES = 4096;
|
|
272
|
+
function validateWorkBuddyHost(input) {
|
|
273
|
+
if (typeof input !== "string" || !input.trim()) {
|
|
274
|
+
return { ok: false, message: "\u8BF7\u8F93\u5165 AE Host\u3002" };
|
|
275
|
+
}
|
|
276
|
+
let url;
|
|
277
|
+
try {
|
|
278
|
+
url = new URL(input.trim());
|
|
279
|
+
} catch {
|
|
280
|
+
return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u7684 HTTPS \u5730\u5740\u3002" };
|
|
281
|
+
}
|
|
282
|
+
if (url.protocol !== "https:") {
|
|
283
|
+
return { ok: false, message: "\u4EC5\u652F\u6301 HTTPS \u5730\u5740\u3002" };
|
|
284
|
+
}
|
|
285
|
+
if (url.username || url.password) {
|
|
286
|
+
return { ok: false, message: "Host \u5730\u5740\u4E0D\u80FD\u5305\u542B\u7528\u6237\u540D\u6216\u5BC6\u7801\u3002" };
|
|
287
|
+
}
|
|
288
|
+
if (url.search || url.hash) {
|
|
289
|
+
return { ok: false, message: "Host \u5730\u5740\u4E0D\u80FD\u5305\u542B\u67E5\u8BE2\u53C2\u6570\u6216\u7247\u6BB5\u3002" };
|
|
290
|
+
}
|
|
291
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
292
|
+
return { ok: true, host: `${url.origin}${pathname === "/" ? "" : pathname}` };
|
|
293
|
+
}
|
|
294
|
+
function renderWorkBuddyHostPage(hosts, sessionPath, nonce) {
|
|
295
|
+
const availableHosts = hosts.filter((host) => validateWorkBuddyHost(host.url).ok);
|
|
296
|
+
const selectedIndex = Math.max(0, availableHosts.findIndex((host) => host.active));
|
|
297
|
+
const hostRows = availableHosts.length > 0 ? availableHosts.map((host, index) => renderHostRow(host, index === selectedIndex)).join("\n") : '<p class="empty">\u8FD8\u6CA1\u6709\u4FDD\u5B58\u7684\u73AF\u5883\uFF0C\u8BF7\u5728\u4E0B\u65B9\u8F93\u5165 AE Host\u3002</p>';
|
|
298
|
+
return `<!doctype html>
|
|
299
|
+
<html lang="zh-CN">
|
|
300
|
+
<head>
|
|
301
|
+
<meta charset="utf-8">
|
|
302
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
303
|
+
<title>ThinkingAI AE \xD7 WorkBuddy</title>
|
|
304
|
+
<style nonce="${nonce}">
|
|
305
|
+
:root { color-scheme: light; font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
|
|
306
|
+
* { box-sizing: border-box; }
|
|
307
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px 20px; color: #111c35; background: #f3f7fb; }
|
|
308
|
+
.card { width: min(640px, 100%); padding: 28px 40px 22px; background: #fff; border: 1px solid #dbe3ee; border-radius: 14px; box-shadow: 0 20px 55px rgba(39, 62, 92, .12); }
|
|
309
|
+
.brands { display: flex; align-items: center; justify-content: center; gap: 16px; min-height: 48px; margin-bottom: 14px; }
|
|
310
|
+
.thinkingai { display: flex; align-items: center; gap: 9px; font-size: 21px; font-weight: 750; letter-spacing: -.02em; }
|
|
311
|
+
.thinkingai img { width: 38px; height: 42px; object-fit: none; object-position: 65% 55%; }
|
|
312
|
+
.workbuddy { width: 132px; height: auto; object-fit: contain; }
|
|
313
|
+
.collaboration { color: #8490a5; font-size: 21px; }
|
|
314
|
+
h1 { margin: 0; text-align: center; font-size: 30px; line-height: 1.25; letter-spacing: -.025em; }
|
|
315
|
+
.subtitle { margin: 7px 0 22px; text-align: center; color: #62708a; font-size: 16px; }
|
|
316
|
+
fieldset { margin: 0; padding: 0; border: 0; }
|
|
317
|
+
legend, .field-label { display: block; margin-bottom: 10px; color: #18243c; font-size: 15px; font-weight: 700; }
|
|
318
|
+
.host-list { display: grid; gap: 8px; max-height: 188px; overflow: auto; }
|
|
319
|
+
.host-option { display: flex; align-items: center; gap: 14px; min-height: 68px; padding: 11px 16px; border: 1px solid #d5deea; border-radius: 8px; cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s; }
|
|
320
|
+
.host-option:hover { border-color: #92b8ff; }
|
|
321
|
+
.host-option:has(input:checked) { border-color: #0878f9; background: #edf5ff; box-shadow: 0 0 0 1px #0878f9 inset; }
|
|
322
|
+
.host-option input { width: 18px; height: 18px; margin: 0; accent-color: #0878f9; flex: none; }
|
|
323
|
+
.host-copy { min-width: 0; display: grid; gap: 3px; }
|
|
324
|
+
.host-label { color: #111827; font-weight: 700; }
|
|
325
|
+
.host-url { overflow: hidden; color: #65738d; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
|
326
|
+
.empty { margin: 0; padding: 18px; color: #65738d; background: #f7f9fc; border: 1px dashed #cad5e3; border-radius: 8px; }
|
|
327
|
+
.divider { display: flex; align-items: center; gap: 16px; margin: 18px 0; color: #7b879d; font-size: 14px; }
|
|
328
|
+
.divider::before, .divider::after { content: ""; height: 1px; background: #dbe3ee; flex: 1; }
|
|
329
|
+
input[type="url"] { width: 100%; height: 46px; padding: 0 14px; color: #162238; font: inherit; border: 1px solid #cad5e3; border-radius: 7px; outline: none; }
|
|
330
|
+
input[type="url"]:focus { border-color: #0878f9; box-shadow: 0 0 0 3px rgba(8, 120, 249, .13); }
|
|
331
|
+
input[type="url"]::placeholder { color: #8a96aa; }
|
|
332
|
+
.helper { margin: 7px 0 0; color: #6b7890; font-size: 13px; }
|
|
333
|
+
.notice { display: flex; align-items: flex-start; gap: 10px; margin: 18px 0; padding: 12px 15px; color: #405579; background: #edf5ff; border-radius: 7px; font-size: 13px; line-height: 1.55; }
|
|
334
|
+
.shield { width: 18px; height: 21px; flex: none; color: #0878f9; }
|
|
335
|
+
.actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
|
|
336
|
+
button { width: 100%; height: 44px; border-radius: 7px; font: inherit; font-weight: 700; cursor: pointer; }
|
|
337
|
+
.primary { color: #fff; background: #0878f9; border: 1px solid #0878f9; }
|
|
338
|
+
.primary:hover { background: #0069df; }
|
|
339
|
+
.secondary { color: #38465d; background: #fff; border: 1px solid #cad5e3; }
|
|
340
|
+
.secondary:hover { background: #f6f8fb; }
|
|
341
|
+
.status { min-height: 22px; margin: 14px 0 0; text-align: center; color: #0a6d3a; font-size: 13px; }
|
|
342
|
+
.status.error { color: #bd1e2d; }
|
|
343
|
+
footer { margin-top: 2px; text-align: center; color: #7b879d; font-size: 12px; font-style: italic; }
|
|
344
|
+
@media (max-width: 620px) { body { padding: 0; background: #fff; } .card { min-height: 100vh; padding: 28px 20px 22px; border: 0; border-radius: 0; box-shadow: none; } .brands { gap: 10px; } .thinkingai { font-size: 17px; } .workbuddy { width: 112px; } h1 { font-size: 26px; } }
|
|
345
|
+
</style>
|
|
346
|
+
</head>
|
|
347
|
+
<body>
|
|
348
|
+
<main class="card">
|
|
349
|
+
<header>
|
|
350
|
+
<div class="brands" aria-label="ThinkingAI AE \u4E0E WorkBuddy">
|
|
351
|
+
<div class="thinkingai"><img src="${sessionPath}thinkingai.png" alt=""><span>ThinkingAI AE</span></div>
|
|
352
|
+
<span class="collaboration" aria-hidden="true">\xD7</span>
|
|
353
|
+
<img class="workbuddy" src="${sessionPath}workbuddy.png" alt="WorkBuddy">
|
|
354
|
+
</div>
|
|
355
|
+
<h1>\u8FDE\u63A5 ThinkingAI AE</h1>
|
|
356
|
+
<p class="subtitle">\u9009\u62E9\u8981\u5728 WorkBuddy \u4E2D\u4F7F\u7528\u7684 AE \u73AF\u5883</p>
|
|
357
|
+
</header>
|
|
358
|
+
<form id="host-form" novalidate>
|
|
359
|
+
<fieldset>
|
|
360
|
+
<legend>\u5DF2\u4FDD\u5B58\u7684\u73AF\u5883</legend>
|
|
361
|
+
<div class="host-list">${hostRows}</div>
|
|
362
|
+
</fieldset>
|
|
363
|
+
<div class="divider">\u6216</div>
|
|
364
|
+
<label class="field-label" for="custom-host">\u4F7F\u7528\u5176\u4ED6 Host</label>
|
|
365
|
+
<input id="custom-host" name="custom-host" type="url" inputmode="url" autocomplete="url" placeholder="https://your-ae-host.example.com">
|
|
366
|
+
<p class="helper">\u4EC5\u652F\u6301 HTTPS \u5730\u5740</p>
|
|
367
|
+
<div class="notice">
|
|
368
|
+
<svg class="shield" viewBox="0 0 24 28" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" d="M12 1.5 21 5v7.2c0 6-3.8 11.2-9 13.8-5.2-2.6-9-7.8-9-13.8V5l9-3.5Z"/></svg>
|
|
369
|
+
<span>\u8D26\u53F7\u5BC6\u7801\u5C06\u5728 AE \u7EDF\u4E00\u767B\u5F55\u9875\u4E2D\u8F93\u5165\uFF0C\u672C\u9875\u9762\u4EC5\u7528\u4E8E\u9009\u62E9 Host\u3002</span>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="actions">
|
|
372
|
+
<button class="primary" type="submit">\u7EE7\u7EED\u524D\u5F80 AE \u767B\u5F55</button>
|
|
373
|
+
<button class="secondary" id="cancel" type="button">\u53D6\u6D88</button>
|
|
374
|
+
</div>
|
|
375
|
+
<p class="status" id="status" role="status" aria-live="polite"></p>
|
|
376
|
+
</form>
|
|
377
|
+
<footer>\u7531 WorkBuddy \xB7 ae-cli \u53D1\u8D77</footer>
|
|
378
|
+
</main>
|
|
379
|
+
<script nonce="${nonce}">
|
|
380
|
+
const form = document.getElementById('host-form');
|
|
381
|
+
const customHost = document.getElementById('custom-host');
|
|
382
|
+
const status = document.getElementById('status');
|
|
383
|
+
const radios = Array.from(document.querySelectorAll('input[name="saved-host"]'));
|
|
384
|
+
|
|
385
|
+
customHost.addEventListener('input', () => {
|
|
386
|
+
if (customHost.value.trim()) radios.forEach(radio => { radio.checked = false; });
|
|
387
|
+
status.textContent = '';
|
|
388
|
+
status.classList.remove('error');
|
|
389
|
+
});
|
|
390
|
+
radios.forEach(radio => radio.addEventListener('change', () => {
|
|
391
|
+
customHost.value = '';
|
|
392
|
+
status.textContent = '';
|
|
393
|
+
status.classList.remove('error');
|
|
394
|
+
}));
|
|
395
|
+
form.addEventListener('submit', async event => {
|
|
396
|
+
event.preventDefault();
|
|
397
|
+
const selected = radios.find(radio => radio.checked);
|
|
398
|
+
const host = customHost.value.trim() || (selected ? selected.value : '');
|
|
399
|
+
status.textContent = '';
|
|
400
|
+
status.classList.remove('error');
|
|
401
|
+
try {
|
|
402
|
+
const response = await fetch('${sessionPath}select', {
|
|
403
|
+
method: 'POST',
|
|
404
|
+
headers: { 'Content-Type': 'application/json' },
|
|
405
|
+
body: JSON.stringify({ host }),
|
|
406
|
+
});
|
|
407
|
+
const result = await response.json();
|
|
408
|
+
if (!response.ok || !result.ok) throw new Error(result.message || 'Host \u6821\u9A8C\u5931\u8D25\u3002');
|
|
409
|
+
status.textContent = '\u5DF2\u4FDD\u5B58 ' + result.host + '\uFF0C\u6B63\u5728\u7EE7\u7EED\u767B\u5F55\u2026';
|
|
410
|
+
} catch (error) {
|
|
411
|
+
status.textContent = error instanceof Error ? error.message : 'Host \u6821\u9A8C\u5931\u8D25\u3002';
|
|
412
|
+
status.classList.add('error');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
document.getElementById('cancel').addEventListener('click', () => {
|
|
416
|
+
status.textContent = '\u5DF2\u53D6\u6D88\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002';
|
|
417
|
+
status.classList.remove('error');
|
|
418
|
+
});
|
|
419
|
+
</script>
|
|
420
|
+
</body>
|
|
421
|
+
</html>`;
|
|
422
|
+
}
|
|
423
|
+
async function startWorkBuddyHostPage(options = {}) {
|
|
424
|
+
const hosts = options.hosts ?? listHosts();
|
|
425
|
+
const sessionId = randomBytes(24).toString("base64url");
|
|
426
|
+
const sessionPath = `/workbuddy/${sessionId}/`;
|
|
427
|
+
const nonce = randomBytes(18).toString("base64url");
|
|
428
|
+
const assets = {
|
|
429
|
+
thinkingai: readBrandAsset("thinkingai.png"),
|
|
430
|
+
workbuddy: readBrandAsset("workbuddy.png")
|
|
431
|
+
};
|
|
432
|
+
let resolveSelection = (_host) => {
|
|
433
|
+
};
|
|
434
|
+
const selection = new Promise((resolve) => {
|
|
435
|
+
resolveSelection = resolve;
|
|
436
|
+
});
|
|
437
|
+
const server = createServer(async (request, response) => {
|
|
438
|
+
setSecurityHeaders(response, nonce);
|
|
439
|
+
const requestUrl = new URL(request.url ?? "/", `http://${LOOPBACK_ADDRESS}`);
|
|
440
|
+
if (request.method === "GET" && requestUrl.pathname === sessionPath) {
|
|
441
|
+
respond(response, 200, "text/html; charset=utf-8", renderWorkBuddyHostPage(hosts, sessionPath, nonce));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (request.method === "GET" && requestUrl.pathname === `${sessionPath}thinkingai.png`) {
|
|
445
|
+
respond(response, 200, "image/png", assets.thinkingai);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (request.method === "GET" && requestUrl.pathname === `${sessionPath}workbuddy.png`) {
|
|
449
|
+
respond(response, 200, "image/png", assets.workbuddy);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (request.method === "POST" && requestUrl.pathname === `${sessionPath}select`) {
|
|
453
|
+
const selectedHost = await handleSelection(request, response, options.onSelect);
|
|
454
|
+
if (selectedHost) resolveSelection(selectedHost);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
respond(response, 404, "application/json; charset=utf-8", JSON.stringify({ ok: false, message: "Not found." }));
|
|
458
|
+
});
|
|
459
|
+
await new Promise((resolve, reject) => {
|
|
460
|
+
server.once("error", reject);
|
|
461
|
+
server.listen(options.port ?? 0, LOOPBACK_ADDRESS, () => {
|
|
462
|
+
server.off("error", reject);
|
|
463
|
+
resolve();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
const address = server.address();
|
|
467
|
+
if (!address || typeof address === "string") {
|
|
468
|
+
await closeServer(server);
|
|
469
|
+
throw new Error("Cannot resolve the WorkBuddy host page address.");
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
url: `http://${LOOPBACK_ADDRESS}:${address.port}${sessionPath}`,
|
|
473
|
+
selection,
|
|
474
|
+
close: () => closeServer(server)
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function renderHostRow(host, selected) {
|
|
478
|
+
const url = escapeHtml(host.url);
|
|
479
|
+
return `<label class="host-option">
|
|
480
|
+
<input type="radio" name="saved-host" value="${url}"${selected ? " checked" : ""}>
|
|
481
|
+
<span class="host-copy"><span class="host-label">${escapeHtml(host.label)}</span><span class="host-url">${url}</span></span>
|
|
482
|
+
</label>`;
|
|
483
|
+
}
|
|
484
|
+
function escapeHtml(value) {
|
|
485
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
486
|
+
}
|
|
487
|
+
function readBrandAsset(name) {
|
|
488
|
+
const candidates = [
|
|
489
|
+
new URL(`../assets/workbuddy-host-page/${name}`, import.meta.url),
|
|
490
|
+
new URL(`../../../assets/workbuddy-host-page/${name}`, import.meta.url)
|
|
491
|
+
];
|
|
492
|
+
const match = candidates.find((candidate) => fs.existsSync(fileURLToPath(candidate)));
|
|
493
|
+
if (!match) throw new Error(`WorkBuddy host page asset is missing: ${name}`);
|
|
494
|
+
return fs.readFileSync(match);
|
|
495
|
+
}
|
|
496
|
+
async function handleSelection(request, response, onSelect) {
|
|
497
|
+
try {
|
|
498
|
+
const body = await readJsonBody(request);
|
|
499
|
+
const result = validateWorkBuddyHost(body.host);
|
|
500
|
+
if (!result.ok) {
|
|
501
|
+
respond(response, 400, "application/json; charset=utf-8", JSON.stringify(result));
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
if (!onSelect) {
|
|
505
|
+
respond(response, 200, "application/json; charset=utf-8", JSON.stringify(result));
|
|
506
|
+
return result.host;
|
|
507
|
+
}
|
|
508
|
+
await onSelect(result.host);
|
|
509
|
+
respond(response, 200, "application/json; charset=utf-8", JSON.stringify(result));
|
|
510
|
+
return result.host;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
const message = error instanceof Error ? error.message : "Invalid request.";
|
|
513
|
+
respond(response, 400, "application/json; charset=utf-8", JSON.stringify({ ok: false, message }));
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function readJsonBody(request) {
|
|
518
|
+
let body = "";
|
|
519
|
+
for await (const chunk of request) {
|
|
520
|
+
body += chunk;
|
|
521
|
+
if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) {
|
|
522
|
+
throw new Error("Request body is too large.");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const parsed = JSON.parse(body || "{}");
|
|
526
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
527
|
+
throw new Error("Request body must be a JSON object.");
|
|
528
|
+
}
|
|
529
|
+
return parsed;
|
|
530
|
+
}
|
|
531
|
+
function setSecurityHeaders(response, nonce) {
|
|
532
|
+
response.setHeader("Cache-Control", "no-store");
|
|
533
|
+
response.setHeader("Content-Security-Policy", `default-src 'none'; img-src 'self'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`);
|
|
534
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
535
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
536
|
+
response.setHeader("X-Frame-Options", "DENY");
|
|
537
|
+
}
|
|
538
|
+
function respond(response, status, contentType, body) {
|
|
539
|
+
response.statusCode = status;
|
|
540
|
+
response.setHeader("Content-Type", contentType);
|
|
541
|
+
response.end(body);
|
|
542
|
+
}
|
|
543
|
+
function closeServer(server) {
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/integrations/workbuddy/auth.ts
|
|
550
|
+
function activateWorkBuddySelectedHost(selectedHost, dependencies = {}) {
|
|
551
|
+
const validation = validateWorkBuddyHost(selectedHost);
|
|
552
|
+
if (!validation.ok) {
|
|
553
|
+
throw new Error(validation.message);
|
|
554
|
+
}
|
|
555
|
+
const host = normalizeUrl(validation.host);
|
|
556
|
+
const readConfig = dependencies.readConfig ?? loadConfig;
|
|
557
|
+
const writeConfig = dependencies.writeConfig ?? saveConfig;
|
|
558
|
+
const config = readConfig();
|
|
559
|
+
if (!config.hosts[host]) {
|
|
560
|
+
const hostname = new URL(host).hostname;
|
|
561
|
+
const labelExists = Object.entries(config.hosts).some(([url, entry]) => url !== host && entry.label === hostname);
|
|
562
|
+
config.hosts[host] = { label: labelExists ? host : hostname };
|
|
563
|
+
}
|
|
564
|
+
config.activeHost = host;
|
|
565
|
+
writeConfig(config);
|
|
566
|
+
logger.info(`Active host set to ${host} for WorkBuddy login`);
|
|
567
|
+
return host;
|
|
568
|
+
}
|
|
569
|
+
function registerWorkBuddyAuth(auth) {
|
|
570
|
+
auth.command("workbuddy-host-page", { hidden: true }).description("Run the local WorkBuddy host selection page").action(async () => {
|
|
571
|
+
const page = await startWorkBuddyHostPage({
|
|
572
|
+
onSelect: async (host) => {
|
|
573
|
+
activateWorkBuddySelectedHost(host);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
process.stderr.write(`[ae-cli] WorkBuddy host selection:
|
|
577
|
+
${page.url}
|
|
578
|
+
`);
|
|
579
|
+
const selected = await Promise.race([
|
|
580
|
+
page.selection.then(() => true),
|
|
581
|
+
waitForTerminationSignal().then(() => false)
|
|
582
|
+
]);
|
|
583
|
+
await page.close();
|
|
584
|
+
if (!selected) process.exitCode = 1;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function waitForTerminationSignal() {
|
|
588
|
+
return new Promise((resolve) => {
|
|
589
|
+
const finish = () => {
|
|
590
|
+
process.off("SIGINT", finish);
|
|
591
|
+
process.off("SIGTERM", finish);
|
|
592
|
+
resolve();
|
|
593
|
+
};
|
|
594
|
+
process.once("SIGINT", finish);
|
|
595
|
+
process.once("SIGTERM", finish);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/commands/auth.ts
|
|
600
|
+
var HOST_OPTION_DESC = "Override active AE host URL (e.g., https://ta.thinkingdata.cn)";
|
|
601
|
+
function getExplicitAuthHostOverride(program, opts) {
|
|
602
|
+
return opts.host || program.opts().host;
|
|
603
|
+
}
|
|
604
|
+
function resolveAuthHost(program, opts) {
|
|
605
|
+
return resolveHost(getExplicitAuthHostOverride(program, opts));
|
|
606
|
+
}
|
|
607
|
+
async function validateExistingCliToken(host, cliToken) {
|
|
608
|
+
try {
|
|
609
|
+
return { valid: true, trusted: false, details: await validateCliTokenOnServer(host, cliToken) };
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (error instanceof CliTokenValidationUnavailableError) {
|
|
612
|
+
logger.warn(`auth status: ${error.message}; trusting the stored CLI token for compatibility`);
|
|
613
|
+
return { valid: true, trusted: true, details: {} };
|
|
614
|
+
}
|
|
615
|
+
if (!(error instanceof SecureStoreAuthError)) throw error;
|
|
616
|
+
printError("auth", error.message);
|
|
617
|
+
process.exitCode = 1;
|
|
618
|
+
return { valid: false, trusted: false, details: {} };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function activateHostAfterLogin(host, explicitHostOverride) {
|
|
622
|
+
if (!explicitHostOverride) return;
|
|
623
|
+
const normalized = normalizeUrl(host);
|
|
624
|
+
const config = loadConfig();
|
|
625
|
+
if (!config.hosts[normalized]) config.hosts[normalized] = { label: normalized };
|
|
626
|
+
config.activeHost = normalized;
|
|
627
|
+
saveConfig(config);
|
|
628
|
+
logger.info(`Active host set to ${normalized} after login`);
|
|
629
|
+
}
|
|
630
|
+
function accountOutput(account) {
|
|
631
|
+
return {
|
|
632
|
+
open_id: account.openId,
|
|
633
|
+
login_name: account.loginName,
|
|
634
|
+
user_name: account.userName
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function credentialOutput(credential, active) {
|
|
638
|
+
return {
|
|
639
|
+
credential_id: credential.id,
|
|
640
|
+
active,
|
|
641
|
+
...credential.account ? { account: accountOutput(credential.account) } : {},
|
|
642
|
+
cli_token: {
|
|
643
|
+
...credential.cliTokenExpiresAt ? { expires_at: credential.cliTokenExpiresAt } : {},
|
|
644
|
+
...credential.lastRenewedOn ? { last_renewed_on: credential.lastRenewedOn } : {}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function buildAccountSelectionItems(credentials, activeCredentialId) {
|
|
649
|
+
return credentials.filter((credential) => Boolean(credential.account)).sort((left, right) => Number(right.id === activeCredentialId) - Number(left.id === activeCredentialId)).map((credential) => {
|
|
650
|
+
const account = credential.account;
|
|
651
|
+
const displayName = account.userName && account.userName !== account.loginName ? `${account.loginName} (${account.userName})` : account.loginName;
|
|
652
|
+
const active = credential.id === activeCredentialId ? " [active]" : "";
|
|
653
|
+
return {
|
|
654
|
+
value: account.openId,
|
|
655
|
+
label: `${displayName} \xB7 ${account.openId}${active}`
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async function switchActiveAccount(program, opts) {
|
|
660
|
+
const host = resolveAuthHost(program, opts);
|
|
661
|
+
if (!host) {
|
|
662
|
+
printError("config", "No AE host configured.", missingAeHostHint());
|
|
663
|
+
process.exitCode = 1;
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
let account = opts.account;
|
|
667
|
+
if (!account) {
|
|
668
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
669
|
+
printError(
|
|
670
|
+
"validation",
|
|
671
|
+
"Interactive account selection requires a TTY.",
|
|
672
|
+
`Run: ae-cli auth list --host ${host}, then ae-cli auth use --host ${host} --account <login-name-or-open-id>`
|
|
673
|
+
);
|
|
674
|
+
process.exitCode = 1;
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const active = getActiveCredential(host);
|
|
678
|
+
const items = buildAccountSelectionItems(listCredentials(host), active?.id);
|
|
679
|
+
if (items.length === 0) {
|
|
680
|
+
printError(
|
|
681
|
+
"auth",
|
|
682
|
+
`No stored accounts with identity metadata for ${host}.`,
|
|
683
|
+
`Upgrade the AE server if needed, then run: ae-cli auth login --host ${host}`
|
|
684
|
+
);
|
|
685
|
+
process.exitCode = 1;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
account = await promptSingleSelect({
|
|
690
|
+
title: `Select an account for ${host} (up/down: move, enter: select, q: cancel)`,
|
|
691
|
+
items
|
|
692
|
+
});
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (error instanceof MultiselectCancelled) return;
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const selected = activateCredential(host, account);
|
|
700
|
+
clearCliTokenCache(host);
|
|
701
|
+
await printOutput({
|
|
702
|
+
host,
|
|
703
|
+
active: true,
|
|
704
|
+
account: accountOutput(selected.account)
|
|
705
|
+
}, program.opts().format || "json");
|
|
706
|
+
} catch (error) {
|
|
707
|
+
printError("auth", error.message);
|
|
708
|
+
process.exitCode = 1;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function persistDeviceTokens(host, tokens, options = {}) {
|
|
712
|
+
const cliToken = await mintCliToken(host, tokens.access_token);
|
|
713
|
+
let validation = {};
|
|
714
|
+
try {
|
|
715
|
+
validation = await validateCliTokenOnServer(host, cliToken);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (error instanceof CliTokenValidationUnavailableError) {
|
|
718
|
+
logger.warn(`auth login: ${error.message}; saving CLI token without account metadata for compatibility`);
|
|
719
|
+
} else {
|
|
720
|
+
clearCliTokenCache(host);
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
let saved;
|
|
725
|
+
try {
|
|
726
|
+
saved = saveCredential(host, {
|
|
727
|
+
cliToken,
|
|
728
|
+
...validation.account ? { account: validation.account } : {},
|
|
729
|
+
...validation.cliTokenExpiresAt ? { cliTokenExpiresAt: validation.cliTokenExpiresAt } : {}
|
|
730
|
+
}, options);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
clearCliTokenCache(host);
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
clearCliTokenCache(host);
|
|
736
|
+
await runHostCompatCheck(getLocalCliPackageInfo(), host);
|
|
737
|
+
return saved;
|
|
738
|
+
}
|
|
739
|
+
function loginSummary(host, credential) {
|
|
740
|
+
return {
|
|
741
|
+
authenticated: true,
|
|
742
|
+
host,
|
|
743
|
+
...credential.account ? { account: accountOutput(credential.account) } : {}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function resolveLoginTeClaudeBase(host) {
|
|
747
|
+
const base = host.replace(/\/+$/, "");
|
|
748
|
+
const basePath = process.env.TE_CLAUDE_BASE_PATH || "/agent";
|
|
749
|
+
return base.endsWith(basePath) ? base : base + basePath;
|
|
750
|
+
}
|
|
751
|
+
function registerAuth(program) {
|
|
752
|
+
const auth = program.command("auth").description("Manage authentication or switch the active account interactively").action(async () => {
|
|
753
|
+
await switchActiveAccount(program, {});
|
|
754
|
+
});
|
|
755
|
+
registerWorkBuddyAuth(auth);
|
|
756
|
+
auth.command("login").description("Log in to AE; by default replaces this host credentials, --add keeps existing accounts").option("--host <url>", HOST_OPTION_DESC).option("--add", "Add another account for the host and keep existing accounts").option("--no-browser", "Do not auto-open the browser; only print the authorization URL").option("--no-wait", "Print device authorization data and exit; resume with --device-code").option("--device-code <code>", "Resume a split-flow login with the given device code").action(async (opts) => {
|
|
757
|
+
const explicitHost = getExplicitAuthHostOverride(program, opts);
|
|
758
|
+
const host = resolveAuthHost(program, opts);
|
|
759
|
+
if (!host) {
|
|
760
|
+
printError("config", "No AE host configured.", missingAeHostHint());
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
const config = loadConfig();
|
|
764
|
+
if (!config.hosts[host]) {
|
|
765
|
+
config.hosts[host] = { label: host };
|
|
766
|
+
if (!config.activeHost) config.activeHost = host;
|
|
767
|
+
saveConfig(config);
|
|
768
|
+
process.stderr.write(`[ae-cli] Config saved for ${host}
|
|
769
|
+
`);
|
|
770
|
+
}
|
|
771
|
+
const noBrowser = !opts.browser;
|
|
772
|
+
const format = program.opts().format || "json";
|
|
773
|
+
const teClaudeBase = resolveLoginTeClaudeBase(host);
|
|
774
|
+
const emit = (message) => process.stderr.write(`[ae-cli] ${message}
|
|
775
|
+
`);
|
|
776
|
+
try {
|
|
777
|
+
if (opts.deviceCode) {
|
|
778
|
+
const tokens2 = await pollDeviceFlow(teClaudeBase, opts.deviceCode, {}, emit);
|
|
779
|
+
const saved2 = await persistDeviceTokens(host, tokens2, { add: opts.add });
|
|
780
|
+
activateHostAfterLogin(host, explicitHost);
|
|
781
|
+
emit("Login successful! CLI token saved securely.");
|
|
782
|
+
await printOutput(loginSummary(host, saved2), format);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (opts.wait === false) {
|
|
786
|
+
const authResp = await authorizeDevice(teClaudeBase);
|
|
787
|
+
const addFlag = opts.add ? " --add" : "";
|
|
788
|
+
await printOutput({
|
|
789
|
+
device_code: authResp.device_code,
|
|
790
|
+
user_code: authResp.user_code,
|
|
791
|
+
verification_url: buildVerificationUrl(teClaudeBase, authResp.user_code),
|
|
792
|
+
interval: authResp.interval,
|
|
793
|
+
expires_in: authResp.expires_in,
|
|
794
|
+
next: `Open verification_url in a browser to authorize, then run: ae-cli auth login${addFlag} --device-code ${authResp.device_code}`
|
|
795
|
+
}, format);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const tokens = await runDeviceFlow(teClaudeBase, { noBrowser }, emit);
|
|
799
|
+
const saved = await persistDeviceTokens(host, tokens, { add: opts.add });
|
|
800
|
+
activateHostAfterLogin(host, explicitHost);
|
|
801
|
+
emit("Login successful! CLI token saved securely.");
|
|
802
|
+
await printOutput(loginSummary(host, saved), format);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (error instanceof DeviceFlowUnsupportedError) {
|
|
805
|
+
printError("auth", error.message, "Upgrade the AE server to a version that supports device login.");
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
if (error instanceof PermissionError) {
|
|
809
|
+
printError("permission", error.message, error.hint, error.code);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
printError("auth", error.message);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
auth.command("status").description("Show the active CLI-token authentication status for a host").option("--host <url>", HOST_OPTION_DESC).action(async (opts) => {
|
|
817
|
+
const host = resolveAuthHost(program, opts);
|
|
818
|
+
if (!host) {
|
|
819
|
+
await printOutput({
|
|
820
|
+
authenticated: false,
|
|
821
|
+
host: "(none)",
|
|
822
|
+
hint: missingAeHostHint(),
|
|
823
|
+
next_steps: missingAeHostGuidance()
|
|
824
|
+
}, program.opts().format || "json");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const active = getActiveCredential(host);
|
|
828
|
+
if (active) {
|
|
829
|
+
const state = await validateExistingCliToken(host, active.cliToken);
|
|
830
|
+
if (!state.valid) return;
|
|
831
|
+
if (state.details.account || state.details.cliTokenExpiresAt) {
|
|
832
|
+
updateCredentialMetadata(host, active.cliToken, state.details);
|
|
833
|
+
}
|
|
834
|
+
const current = getActiveCredential(host) ?? active;
|
|
835
|
+
await printOutput({
|
|
836
|
+
authenticated: true,
|
|
837
|
+
host,
|
|
838
|
+
source: "secure-store",
|
|
839
|
+
cli_token: {
|
|
840
|
+
status: state.trusted ? "trusted" : "valid",
|
|
841
|
+
...current.cliTokenExpiresAt ? { expires_at: current.cliTokenExpiresAt } : {},
|
|
842
|
+
...current.lastRenewedOn ? { last_renewed_on: current.lastRenewedOn } : {}
|
|
843
|
+
},
|
|
844
|
+
...current.account ? { account: accountOutput(current.account) } : {}
|
|
845
|
+
}, program.opts().format || "json");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const fallback = getFallbackCliToken(host);
|
|
849
|
+
if (fallback) {
|
|
850
|
+
const state = await validateExistingCliToken(host, fallback);
|
|
851
|
+
if (!state.valid) return;
|
|
852
|
+
await printOutput({
|
|
853
|
+
authenticated: true,
|
|
854
|
+
host,
|
|
855
|
+
source: "sandbox-cli-token",
|
|
856
|
+
cli_token: {
|
|
857
|
+
status: state.trusted ? "trusted" : "valid",
|
|
858
|
+
...state.details.cliTokenExpiresAt ? { expires_at: state.details.cliTokenExpiresAt } : {}
|
|
859
|
+
},
|
|
860
|
+
...state.details.account ? { account: accountOutput(state.details.account) } : {}
|
|
861
|
+
}, program.opts().format || "json");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
await printOutput({ authenticated: false, host, source: "none" }, program.opts().format || "json");
|
|
865
|
+
});
|
|
866
|
+
auth.command("list").description("List stored accounts for one host (does not call the server)").option("--host <url>", HOST_OPTION_DESC).action(async (opts) => {
|
|
867
|
+
const host = resolveAuthHost(program, opts);
|
|
868
|
+
if (!host) {
|
|
869
|
+
printError("config", "No AE host configured.", missingAeHostHint());
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
const active = getActiveCredential(host);
|
|
873
|
+
const credentials = listCredentials(host);
|
|
874
|
+
await printOutput({
|
|
875
|
+
host,
|
|
876
|
+
accounts: credentials.map((credential) => credentialOutput(credential, credential.id === active?.id))
|
|
877
|
+
}, program.opts().format || "json");
|
|
878
|
+
});
|
|
879
|
+
auth.command("use").description("Switch the active account for a host; omit --account to choose interactively").option("--account <login-name-or-open-id>", "Login name or open_id returned by auth list").option("--host <url>", HOST_OPTION_DESC).action(async (opts) => {
|
|
880
|
+
await switchActiveAccount(program, opts);
|
|
881
|
+
});
|
|
882
|
+
auth.command("logout").description("Remove the active account, or all accounts with --all").option("--host <url>", HOST_OPTION_DESC).option("--all", "Remove all accounts stored for this host").action(async (opts) => {
|
|
883
|
+
const host = resolveAuthHost(program, opts);
|
|
884
|
+
if (!host) {
|
|
885
|
+
printError("config", "No AE host configured.", missingAeHostHint());
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
clearCliTokenCache(host);
|
|
889
|
+
if (opts.all) {
|
|
890
|
+
clearAllCredentials(host);
|
|
891
|
+
await printOutput({ cleared: true, all: true, host }, program.opts().format || "json");
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const result = removeActiveCredential(host);
|
|
895
|
+
await printOutput({
|
|
896
|
+
cleared: !!result.removed,
|
|
897
|
+
all: false,
|
|
898
|
+
host,
|
|
899
|
+
remaining_accounts: listCredentials(host).length,
|
|
900
|
+
...result.active?.account ? { active_account: accountOutput(result.active.account) } : {}
|
|
901
|
+
}, program.opts().format || "json");
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
export {
|
|
905
|
+
activateHostAfterLogin,
|
|
906
|
+
buildAccountSelectionItems,
|
|
907
|
+
persistDeviceTokens,
|
|
908
|
+
registerAuth,
|
|
909
|
+
resolveLoginTeClaudeBase
|
|
910
|
+
};
|