@zhoujinandrew/te-cli 1.0.0
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 +109 -0
- package/bin/te-cli.js +2 -0
- package/dist/analysis-SFPXKD3E.js +218 -0
- package/dist/audience-AZ72TU5F.js +119 -0
- package/dist/auth-2OVPSXHK.js +19 -0
- package/dist/auth-6QCDAUYV.js +67 -0
- package/dist/auth-CFVQVVQN.js +51 -0
- package/dist/auth-QNSRS5ZR.js +19 -0
- package/dist/auth-V4MDYFHU.js +67 -0
- package/dist/auth-Z652VWDK.js +19 -0
- package/dist/chunk-4YPCK7T5.js +217 -0
- package/dist/chunk-C3VJLU5Y.js +206 -0
- package/dist/chunk-CFCHSAMQ.js +108 -0
- package/dist/chunk-KA66K7EL.js +144 -0
- package/dist/chunk-KAWQCMDL.js +44 -0
- package/dist/chunk-KM57HI5B.js +107 -0
- package/dist/chunk-KNKRSOCY.js +144 -0
- package/dist/chunk-P7NQZGSZ.js +217 -0
- package/dist/chunk-STTYG7WN.js +91 -0
- package/dist/chunk-V3D6XL5Z.js +134 -0
- package/dist/client-CSJ3XBC4.js +16 -0
- package/dist/client-EZSKQFIH.js +16 -0
- package/dist/client-OT7PTMI2.js +16 -0
- package/dist/config-53KUB7OG.js +146 -0
- package/dist/config-QQE7LMPZ.js +40 -0
- package/dist/config-S53L4MQK.js +242 -0
- package/dist/index.js +239 -0
- package/dist/meta-ZXBX5RER.js +135 -0
- package/dist/operation-6POPVVIZ.js +214 -0
- package/dist/raw-76GSW4Q4.js +59 -0
- package/dist/raw-K2FR7QAI.js +55 -0
- package/dist/raw-YVFTDHSI.js +59 -0
- package/package.json +40 -0
- package/skills/te-analysis/SKILL.md +93 -0
- package/skills/te-analysis/references/create-dashboard.md +30 -0
- package/skills/te-analysis/references/get-dashboard.md +24 -0
- package/skills/te-analysis/references/get-report.md +24 -0
- package/skills/te-analysis/references/list-dashboard-reports.md +24 -0
- package/skills/te-analysis/references/list-dashboards.md +23 -0
- package/skills/te-analysis/references/list-reports.md +26 -0
- package/skills/te-analysis/references/query-report-data.md +34 -0
- package/skills/te-analysis/references/query-sql.md +31 -0
- package/skills/te-analysis/references/save-report.md +42 -0
- package/skills/te-analysis/references/update-dashboard.md +33 -0
- package/skills/te-audience/SKILL.md +60 -0
- package/skills/te-audience/references/te-audience-get-tag.md +21 -0
- package/skills/te-audience/references/te-audience-list-audience-events.md +20 -0
- package/skills/te-audience/references/te-audience-list-clusters.md +20 -0
- package/skills/te-audience/references/te-audience-list-tags.md +23 -0
- package/skills/te-audience/references/te-audience-load-audience-props.md +25 -0
- package/skills/te-audience/references/te-audience-predict-cluster-count.md +22 -0
- package/skills/te-meta/SKILL.md +80 -0
- package/skills/te-meta/references/get-table-columns.md +29 -0
- package/skills/te-meta/references/list-entities.md +26 -0
- package/skills/te-meta/references/list-events.md +29 -0
- package/skills/te-meta/references/list-metrics.md +26 -0
- package/skills/te-meta/references/list-tables.md +26 -0
- package/skills/te-meta/references/load-event-props.md +27 -0
- package/skills/te-meta/references/load-measure-props.md +24 -0
- package/skills/te-operation/SKILL.md +75 -0
- package/skills/te-operation/references/te-operation-create-task.md +29 -0
- package/skills/te-operation/references/te-operation-get-channel.md +20 -0
- package/skills/te-operation/references/te-operation-get-flow.md +20 -0
- package/skills/te-operation/references/te-operation-get-space-tree.md +19 -0
- package/skills/te-operation/references/te-operation-get-task-stats.md +20 -0
- package/skills/te-operation/references/te-operation-get-timezone.md +19 -0
- package/skills/te-operation/references/te-operation-list-channels.md +19 -0
- package/skills/te-operation/references/te-operation-list-flows.md +19 -0
- package/skills/te-operation/references/te-operation-list-mark-times.md +19 -0
- package/skills/te-operation/references/te-operation-list-tasks.md +25 -0
- package/skills/te-operation/references/te-operation-save-flow.md +29 -0
- package/skills/te-shared/SKILL.md +115 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var CONFIG_DIR = path.join(process.env.HOME || "", ".te-cli");
|
|
5
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return { defaultHost: "ta.thinkingdata.cn", hosts: {} };
|
|
17
|
+
}
|
|
18
|
+
function saveConfig(config) {
|
|
19
|
+
ensureDir();
|
|
20
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
21
|
+
}
|
|
22
|
+
function getDefaultHost() {
|
|
23
|
+
return process.env.TE_HOST || loadConfig().defaultHost || "ta.thinkingdata.cn";
|
|
24
|
+
}
|
|
25
|
+
function setConfigValue(key, value) {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
if (key === "defaultHost") {
|
|
28
|
+
config.defaultHost = value;
|
|
29
|
+
} else {
|
|
30
|
+
config[key] = value;
|
|
31
|
+
}
|
|
32
|
+
saveConfig(config);
|
|
33
|
+
}
|
|
34
|
+
function getConfigDir() {
|
|
35
|
+
return CONFIG_DIR;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
loadConfig,
|
|
40
|
+
saveConfig,
|
|
41
|
+
getDefaultHost,
|
|
42
|
+
setConfigValue,
|
|
43
|
+
getConfigDir
|
|
44
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/framework/output.ts
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
var KNOWN_ARRAY_FIELDS = [
|
|
4
|
+
"items",
|
|
5
|
+
"events",
|
|
6
|
+
"reports",
|
|
7
|
+
"dashboards",
|
|
8
|
+
"tags",
|
|
9
|
+
"clusters",
|
|
10
|
+
"flows",
|
|
11
|
+
"tasks",
|
|
12
|
+
"channels",
|
|
13
|
+
"nodes",
|
|
14
|
+
"members",
|
|
15
|
+
"records",
|
|
16
|
+
"entities",
|
|
17
|
+
"metrics",
|
|
18
|
+
"tables",
|
|
19
|
+
"columns",
|
|
20
|
+
"properties"
|
|
21
|
+
];
|
|
22
|
+
function findArrayField(data) {
|
|
23
|
+
if (Array.isArray(data)) return data;
|
|
24
|
+
if (typeof data !== "object" || data === null) return null;
|
|
25
|
+
for (const key of KNOWN_ARRAY_FIELDS) {
|
|
26
|
+
if (Array.isArray(data[key])) return data[key];
|
|
27
|
+
}
|
|
28
|
+
for (const key of Object.keys(data)) {
|
|
29
|
+
if (Array.isArray(data[key])) return data[key];
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function formatTable(data) {
|
|
34
|
+
const rows = findArrayField(data);
|
|
35
|
+
if (!rows || rows.length === 0) {
|
|
36
|
+
return JSON.stringify(data, null, 2);
|
|
37
|
+
}
|
|
38
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
if (typeof row === "object" && row !== null) {
|
|
41
|
+
Object.keys(row).forEach((k) => allKeys.add(k));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const keys = Array.from(allKeys);
|
|
45
|
+
if (keys.length === 0) return JSON.stringify(rows, null, 2);
|
|
46
|
+
const table = new Table({ head: keys, wordWrap: true });
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
table.push(keys.map((k) => {
|
|
49
|
+
const v = row?.[k];
|
|
50
|
+
if (v === void 0 || v === null) return "";
|
|
51
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
52
|
+
return String(v);
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
return table.toString();
|
|
56
|
+
}
|
|
57
|
+
function applyJq(data, expr) {
|
|
58
|
+
if (!expr) return data;
|
|
59
|
+
const parts = expr.replace(/^\.(data\.)?/, "").split(".");
|
|
60
|
+
let result = data;
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
const arrayMatch = part.match(/^(.+)\[\]$/);
|
|
63
|
+
if (arrayMatch) {
|
|
64
|
+
result = result?.[arrayMatch[1]];
|
|
65
|
+
if (!Array.isArray(result)) return result;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const indexMatch = part.match(/^(.+)\[(\d+)\]$/);
|
|
69
|
+
if (indexMatch) {
|
|
70
|
+
result = result?.[indexMatch[1]]?.[parseInt(indexMatch[2])];
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (part) {
|
|
74
|
+
result = result?.[part];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function formatOutput(data, format, jqExpr) {
|
|
80
|
+
let processed = data;
|
|
81
|
+
if (jqExpr) {
|
|
82
|
+
processed = applyJq(data, jqExpr);
|
|
83
|
+
}
|
|
84
|
+
if (format === "table") {
|
|
85
|
+
return formatTable(processed);
|
|
86
|
+
}
|
|
87
|
+
const envelope = { ok: true, data: processed };
|
|
88
|
+
return JSON.stringify(envelope, null, 2);
|
|
89
|
+
}
|
|
90
|
+
function formatError(type, message, hint, code) {
|
|
91
|
+
const envelope = {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: { type, message, hint, code }
|
|
94
|
+
};
|
|
95
|
+
return JSON.stringify(envelope, null, 2);
|
|
96
|
+
}
|
|
97
|
+
function printError(type, message, hint, code) {
|
|
98
|
+
process.stderr.write(formatError(type, message, hint, code) + "\n");
|
|
99
|
+
}
|
|
100
|
+
function printOutput(data, format, jqExpr) {
|
|
101
|
+
process.stdout.write(formatOutput(data, format, jqExpr) + "\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
printError,
|
|
106
|
+
printOutput
|
|
107
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearToken,
|
|
3
|
+
getToken,
|
|
4
|
+
resolveHost
|
|
5
|
+
} from "./chunk-P7NQZGSZ.js";
|
|
6
|
+
|
|
7
|
+
// src/core/client.ts
|
|
8
|
+
import WebSocket from "ws";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
10
|
+
function genRequestId(prefix) {
|
|
11
|
+
const rand = randomBytes(4).toString("base64url").slice(0, 8);
|
|
12
|
+
return `${prefix}@@${rand}`;
|
|
13
|
+
}
|
|
14
|
+
function buildUrl(baseUrl, modulePath, params = {}) {
|
|
15
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
16
|
+
const p = modulePath.startsWith("/") ? modulePath : `/${modulePath}`;
|
|
17
|
+
const url = new URL(`${base}${p}`);
|
|
18
|
+
for (const [k, v] of Object.entries(params)) {
|
|
19
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
20
|
+
}
|
|
21
|
+
return url.toString();
|
|
22
|
+
}
|
|
23
|
+
function buildWsUrl(baseUrl, token) {
|
|
24
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
25
|
+
const wsBase = base.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
26
|
+
return `${wsBase}/v1/ta-websocket/query/${token}`;
|
|
27
|
+
}
|
|
28
|
+
async function request(method, modulePath, params = {}, body = null, retry = true, hostUrl) {
|
|
29
|
+
const resolvedHost = resolveHost(hostUrl);
|
|
30
|
+
const token = await getToken(resolvedHost);
|
|
31
|
+
const url = buildUrl(resolvedHost, modulePath, params);
|
|
32
|
+
const headers = {
|
|
33
|
+
"Authorization": `bearer ${token}`,
|
|
34
|
+
"Content-Type": "application/json"
|
|
35
|
+
};
|
|
36
|
+
const options = { method, headers };
|
|
37
|
+
if (body && method !== "GET") options.body = JSON.stringify(body);
|
|
38
|
+
const resp = await fetch(url, options);
|
|
39
|
+
if ((resp.status === 401 || resp.status === 403) && retry) {
|
|
40
|
+
clearToken(resolvedHost);
|
|
41
|
+
return request(method, modulePath, params, body, false, resolvedHost);
|
|
42
|
+
}
|
|
43
|
+
const data = await resp.json();
|
|
44
|
+
if (data.return_code === -1001 && retry) {
|
|
45
|
+
clearToken(resolvedHost);
|
|
46
|
+
return request(method, modulePath, params, body, false, resolvedHost);
|
|
47
|
+
}
|
|
48
|
+
if (data.return_code !== 0 && data.return_code !== void 0) {
|
|
49
|
+
throw new Error(`TE API error: ${data.return_message || "unknown"} (code: ${data.return_code})`);
|
|
50
|
+
}
|
|
51
|
+
return data.data !== void 0 ? data.data : data;
|
|
52
|
+
}
|
|
53
|
+
async function httpGet(modulePath, params = {}, hostUrl) {
|
|
54
|
+
return request("GET", modulePath, params, null, true, hostUrl);
|
|
55
|
+
}
|
|
56
|
+
async function httpPost(modulePath, params = {}, body, hostUrl) {
|
|
57
|
+
return request("POST", modulePath, params, body ?? {}, true, hostUrl);
|
|
58
|
+
}
|
|
59
|
+
async function wsQueryOnce(projectId, requestId, qp, eventModel, options = {}, token, hostUrl, timeoutMs = 3e4) {
|
|
60
|
+
const wsUrl = buildWsUrl(hostUrl, token);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const ws = new WebSocket(wsUrl);
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
ws.close();
|
|
65
|
+
reject(new Error(`WebSocket query timed out after ${timeoutMs}ms`));
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
ws.on("open", () => {
|
|
68
|
+
const payload = {
|
|
69
|
+
requestId,
|
|
70
|
+
projectId,
|
|
71
|
+
eventModel,
|
|
72
|
+
qp: typeof qp === "string" ? qp : JSON.stringify(qp),
|
|
73
|
+
searchSource: options.searchSource || "reportQuery",
|
|
74
|
+
querySource: options.querySource || "single_report",
|
|
75
|
+
contentTranslate: options.contentTranslate !== false,
|
|
76
|
+
...options.extra || {}
|
|
77
|
+
};
|
|
78
|
+
ws.send(JSON.stringify(["data", payload, { channel: "ta" }]));
|
|
79
|
+
});
|
|
80
|
+
ws.on("message", (raw) => {
|
|
81
|
+
try {
|
|
82
|
+
const msg = JSON.parse(raw.toString());
|
|
83
|
+
if (Array.isArray(msg) && msg.length >= 2) {
|
|
84
|
+
const data = msg[1];
|
|
85
|
+
if (data.requestId === requestId && data.progress === 100) {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
ws.close();
|
|
88
|
+
resolve(data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
ws.on("error", (err) => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
reject(err);
|
|
97
|
+
});
|
|
98
|
+
ws.on("close", () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function wsQuery(projectId, requestId, qp, eventModel, options = {}, hostUrl) {
|
|
104
|
+
const resolvedHost = resolveHost(hostUrl);
|
|
105
|
+
const token = await getToken(resolvedHost);
|
|
106
|
+
try {
|
|
107
|
+
return await wsQueryOnce(projectId, requestId, qp, eventModel, options, token, resolvedHost);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const msg = err.message || "";
|
|
110
|
+
if (msg.includes("401") || msg.includes("403") || msg.includes("auth")) {
|
|
111
|
+
clearToken(resolvedHost);
|
|
112
|
+
const newToken = await getToken(resolvedHost);
|
|
113
|
+
return wsQueryOnce(projectId, requestId, qp, eventModel, options, newToken, resolvedHost);
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function querySql(projectId, sql, hostUrl) {
|
|
119
|
+
const requestId = genRequestId("sqlIde");
|
|
120
|
+
const qp = {
|
|
121
|
+
events: { sql },
|
|
122
|
+
eventView: { sqlViewParams: [] }
|
|
123
|
+
};
|
|
124
|
+
return wsQuery(projectId, requestId, qp, 10, {
|
|
125
|
+
searchSource: "sqlIde",
|
|
126
|
+
querySource: "sqlIde"
|
|
127
|
+
}, hostUrl);
|
|
128
|
+
}
|
|
129
|
+
async function queryReportData(projectId, reportId, qp, eventModel, options = {}, hostUrl) {
|
|
130
|
+
const requestId = genRequestId("reportQuery");
|
|
131
|
+
return wsQuery(projectId, requestId, qp, eventModel, {
|
|
132
|
+
searchSource: "reportQuery",
|
|
133
|
+
querySource: "single_report",
|
|
134
|
+
...options
|
|
135
|
+
}, hostUrl);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
httpGet,
|
|
140
|
+
httpPost,
|
|
141
|
+
wsQuery,
|
|
142
|
+
querySql,
|
|
143
|
+
queryReportData
|
|
144
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractHostname,
|
|
3
|
+
getActiveHost,
|
|
4
|
+
getConfigDir
|
|
5
|
+
} from "./chunk-CFCHSAMQ.js";
|
|
6
|
+
|
|
7
|
+
// src/core/auth.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
var TOKENS_FILE = path.join(getConfigDir(), "tokens.json");
|
|
12
|
+
var LEGACY_DIR = path.join(process.env.HOME || "", ".te-mcp");
|
|
13
|
+
var TOKEN_TTL_MS = 20 * 60 * 60 * 1e3;
|
|
14
|
+
var OSASCRIPT_POLL_INTERVAL_MS = 2e3;
|
|
15
|
+
var OSASCRIPT_POLL_TIMEOUT_MS = 6e4;
|
|
16
|
+
function ensureDir() {
|
|
17
|
+
const dir = getConfigDir();
|
|
18
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
function resolveHost(hostOverride) {
|
|
21
|
+
if (hostOverride) return hostOverride;
|
|
22
|
+
return getActiveHost();
|
|
23
|
+
}
|
|
24
|
+
function loadAllTokens() {
|
|
25
|
+
try {
|
|
26
|
+
const legacyTokens = path.join(LEGACY_DIR, "tokens.json");
|
|
27
|
+
if (fs.existsSync(legacyTokens) && !fs.existsSync(TOKENS_FILE)) {
|
|
28
|
+
const data = JSON.parse(fs.readFileSync(legacyTokens, "utf-8"));
|
|
29
|
+
const migrated = {};
|
|
30
|
+
for (const [key, val] of Object.entries(data)) {
|
|
31
|
+
const url = key.startsWith("http") ? key : `https://${key}`;
|
|
32
|
+
migrated[url] = val;
|
|
33
|
+
}
|
|
34
|
+
ensureDir();
|
|
35
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(migrated, null, 2));
|
|
36
|
+
return migrated;
|
|
37
|
+
}
|
|
38
|
+
if (fs.existsSync(TOKENS_FILE)) {
|
|
39
|
+
const data = JSON.parse(fs.readFileSync(TOKENS_FILE, "utf-8"));
|
|
40
|
+
let needsMigration = false;
|
|
41
|
+
const migrated = {};
|
|
42
|
+
for (const [key, val] of Object.entries(data)) {
|
|
43
|
+
if (!key.startsWith("http")) {
|
|
44
|
+
migrated[`https://${key}`] = val;
|
|
45
|
+
needsMigration = true;
|
|
46
|
+
} else {
|
|
47
|
+
migrated[key] = val;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (needsMigration) {
|
|
51
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(migrated, null, 2));
|
|
52
|
+
return migrated;
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
function saveAllTokens(tokens) {
|
|
61
|
+
ensureDir();
|
|
62
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2));
|
|
63
|
+
}
|
|
64
|
+
function loadToken(hostUrl) {
|
|
65
|
+
const tokens = loadAllTokens();
|
|
66
|
+
const entry = tokens[hostUrl];
|
|
67
|
+
if (!entry || !entry.token) return null;
|
|
68
|
+
if (entry.updatedAt) {
|
|
69
|
+
const age = Date.now() - new Date(entry.updatedAt).getTime();
|
|
70
|
+
if (age > TOKEN_TTL_MS) {
|
|
71
|
+
clearToken(hostUrl);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { host: hostUrl, token: entry.token, updatedAt: entry.updatedAt };
|
|
76
|
+
}
|
|
77
|
+
function saveToken(token, hostUrl) {
|
|
78
|
+
const tokens = loadAllTokens();
|
|
79
|
+
tokens[hostUrl] = { token, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
80
|
+
saveAllTokens(tokens);
|
|
81
|
+
}
|
|
82
|
+
function clearToken(hostUrl) {
|
|
83
|
+
const tokens = loadAllTokens();
|
|
84
|
+
delete tokens[hostUrl];
|
|
85
|
+
saveAllTokens(tokens);
|
|
86
|
+
}
|
|
87
|
+
function setTokenManual(token, hostUrl) {
|
|
88
|
+
saveToken(token, hostUrl);
|
|
89
|
+
}
|
|
90
|
+
function getAuthStatus(hostUrl) {
|
|
91
|
+
if (process.env.TE_TOKEN) {
|
|
92
|
+
return { authenticated: true, host: hostUrl, source: "env:TE_TOKEN" };
|
|
93
|
+
}
|
|
94
|
+
const cached = loadToken(hostUrl);
|
|
95
|
+
if (cached) {
|
|
96
|
+
const ageMs = Date.now() - new Date(cached.updatedAt).getTime();
|
|
97
|
+
const hours = Math.round(ageMs / 36e5);
|
|
98
|
+
return { authenticated: true, host: hostUrl, tokenAge: `${hours}h ago`, source: "cache" };
|
|
99
|
+
}
|
|
100
|
+
return { authenticated: false, host: hostUrl };
|
|
101
|
+
}
|
|
102
|
+
function extractTokenViaOsascript(hostUrl) {
|
|
103
|
+
if (process.platform !== "darwin") return { token: null, error: "not_mac" };
|
|
104
|
+
const hostname = extractHostname(hostUrl);
|
|
105
|
+
const lines = [
|
|
106
|
+
'tell application "Google Chrome"',
|
|
107
|
+
" repeat with w in windows",
|
|
108
|
+
" repeat with t in tabs of w",
|
|
109
|
+
` if URL of t contains "${hostname}" then`,
|
|
110
|
+
` return execute t javascript "localStorage.getItem('ACCESS_TOKEN')"`,
|
|
111
|
+
" end if",
|
|
112
|
+
" end repeat",
|
|
113
|
+
" end repeat",
|
|
114
|
+
' return "NO_TAB_FOUND"',
|
|
115
|
+
"end tell"
|
|
116
|
+
];
|
|
117
|
+
try {
|
|
118
|
+
const args = lines.flatMap((line) => ["-e", line]);
|
|
119
|
+
const result = execFileSync("osascript", args, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
120
|
+
if (result === "NO_TAB_FOUND") return { token: null, error: "no_tab" };
|
|
121
|
+
if (!result || result === "missing value") return { token: null, error: "no_token" };
|
|
122
|
+
return { token: result.replace(/^["']|["']$/g, ""), error: null };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const msg = e.message || "";
|
|
125
|
+
if (msg.includes("not allowed") || msg.includes("assistive access") || msg.includes("(-1743)")) {
|
|
126
|
+
return { token: null, error: "no_js_permission" };
|
|
127
|
+
}
|
|
128
|
+
return { token: null, error: "no_tab" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function requestTokenViaOsascript(hostUrl) {
|
|
132
|
+
process.stderr.write(`[te-cli] No TE tab found in Chrome. Opening ${hostUrl} ...
|
|
133
|
+
`);
|
|
134
|
+
process.stderr.write(`[te-cli] Please login, then your token will be captured automatically.
|
|
135
|
+
`);
|
|
136
|
+
try {
|
|
137
|
+
const openScript = [
|
|
138
|
+
'tell application "Google Chrome"',
|
|
139
|
+
" activate",
|
|
140
|
+
" if (count of windows) > 0 then",
|
|
141
|
+
` make new tab at end of tabs of window 1 with properties {URL:"${hostUrl}"}`,
|
|
142
|
+
" else",
|
|
143
|
+
` open location "${hostUrl}"`,
|
|
144
|
+
" end if",
|
|
145
|
+
"end tell"
|
|
146
|
+
];
|
|
147
|
+
execFileSync("osascript", openScript.flatMap((line) => ["-e", line]), { timeout: 5e3 });
|
|
148
|
+
} catch {
|
|
149
|
+
process.stderr.write(`[te-cli] Could not open browser. Please open ${hostUrl} in Chrome manually.
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
const deadline = Date.now() + OSASCRIPT_POLL_TIMEOUT_MS;
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
await new Promise((r) => setTimeout(r, OSASCRIPT_POLL_INTERVAL_MS));
|
|
155
|
+
const { token, error } = extractTokenViaOsascript(hostUrl);
|
|
156
|
+
if (token) {
|
|
157
|
+
process.stderr.write(`[te-cli] Token captured from Chrome for ${hostUrl}.
|
|
158
|
+
`);
|
|
159
|
+
return token;
|
|
160
|
+
}
|
|
161
|
+
if (error === "no_js_permission") return null;
|
|
162
|
+
}
|
|
163
|
+
process.stderr.write(`[te-cli] Polling timed out after ${OSASCRIPT_POLL_TIMEOUT_MS / 1e3}s.
|
|
164
|
+
`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
async function getToken(hostUrl) {
|
|
168
|
+
if (!hostUrl) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`No TE host configured.
|
|
171
|
+
Run: te-cli config set-host`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (process.env.TE_TOKEN) {
|
|
175
|
+
return process.env.TE_TOKEN;
|
|
176
|
+
}
|
|
177
|
+
const cached = loadToken(hostUrl);
|
|
178
|
+
if (cached && cached.token) return cached.token;
|
|
179
|
+
const { token: osascriptToken, error } = extractTokenViaOsascript(hostUrl);
|
|
180
|
+
if (error === "no_js_permission") {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Chrome JavaScript from Apple Events is not enabled.
|
|
183
|
+
Enable it: Chrome menu \u2192 View \u2192 Developer \u2192 Allow JavaScript from Apple Events`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (error === "not_mac") {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Auto token extraction is only available on macOS + Chrome.
|
|
189
|
+
Use: te-cli auth set-token <token>`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (osascriptToken) {
|
|
193
|
+
saveToken(osascriptToken, hostUrl);
|
|
194
|
+
return osascriptToken;
|
|
195
|
+
}
|
|
196
|
+
const polledToken = await requestTokenViaOsascript(hostUrl);
|
|
197
|
+
if (polledToken) {
|
|
198
|
+
saveToken(polledToken, hostUrl);
|
|
199
|
+
return polledToken;
|
|
200
|
+
}
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Cannot obtain token for ${hostUrl}.
|
|
203
|
+
Options:
|
|
204
|
+
1. te-cli auth login (macOS + Chrome)
|
|
205
|
+
2. te-cli auth set-token <token>`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
resolveHost,
|
|
211
|
+
loadToken,
|
|
212
|
+
saveToken,
|
|
213
|
+
clearToken,
|
|
214
|
+
setTokenManual,
|
|
215
|
+
getAuthStatus,
|
|
216
|
+
getToken
|
|
217
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var CONFIG_DIR = path.join(process.env.HOME || "", ".te-cli");
|
|
5
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
13
|
+
if (raw.defaultHost && !raw.activeHost) {
|
|
14
|
+
const migrated = migrateConfig(raw);
|
|
15
|
+
saveConfig(migrated);
|
|
16
|
+
return migrated;
|
|
17
|
+
}
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return { activeHost: "", hosts: {} };
|
|
23
|
+
}
|
|
24
|
+
function migrateConfig(old) {
|
|
25
|
+
const hosts = {};
|
|
26
|
+
const oldHost = old.defaultHost;
|
|
27
|
+
if (old.hosts) {
|
|
28
|
+
for (const [key, val] of Object.entries(old.hosts)) {
|
|
29
|
+
const url = key.startsWith("http") ? key : `https://${key}`;
|
|
30
|
+
hosts[url] = val;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const activeHost = oldHost.startsWith("http") ? oldHost : `https://${oldHost}`;
|
|
34
|
+
if (!hosts[activeHost]) {
|
|
35
|
+
hosts[activeHost] = { label: "default" };
|
|
36
|
+
}
|
|
37
|
+
return { activeHost, hosts };
|
|
38
|
+
}
|
|
39
|
+
function saveConfig(config) {
|
|
40
|
+
ensureDir();
|
|
41
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
42
|
+
}
|
|
43
|
+
function getActiveHost() {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
return config.activeHost;
|
|
46
|
+
}
|
|
47
|
+
function setActiveHost(url) {
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
if (!config.hosts[url]) {
|
|
50
|
+
throw new Error(`Host ${url} is not in the configured hosts list. Add it first.`);
|
|
51
|
+
}
|
|
52
|
+
config.activeHost = url;
|
|
53
|
+
saveConfig(config);
|
|
54
|
+
}
|
|
55
|
+
function addHost(url, label) {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
config.hosts[url] = { label };
|
|
58
|
+
if (!config.activeHost) {
|
|
59
|
+
config.activeHost = url;
|
|
60
|
+
}
|
|
61
|
+
saveConfig(config);
|
|
62
|
+
}
|
|
63
|
+
function listHosts() {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
return Object.entries(config.hosts).map(([url, entry]) => ({
|
|
66
|
+
url,
|
|
67
|
+
label: entry.label,
|
|
68
|
+
active: url === config.activeHost
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function getConfigDir() {
|
|
72
|
+
return CONFIG_DIR;
|
|
73
|
+
}
|
|
74
|
+
function extractHostname(fullUrl) {
|
|
75
|
+
try {
|
|
76
|
+
const u = new URL(fullUrl);
|
|
77
|
+
return u.host;
|
|
78
|
+
} catch {
|
|
79
|
+
return fullUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
loadConfig,
|
|
85
|
+
getActiveHost,
|
|
86
|
+
setActiveHost,
|
|
87
|
+
addHost,
|
|
88
|
+
listHosts,
|
|
89
|
+
getConfigDir,
|
|
90
|
+
extractHostname
|
|
91
|
+
};
|