@super-coding/cli 0.0.0-stage → 0.2.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 +70 -2
- package/dist/cli.js +57 -0
- package/dist/daemon/adapters/base.js +37 -0
- package/dist/daemon/adapters/claude-code.js +184 -0
- package/dist/daemon/adapters/cursor.js +138 -0
- package/dist/daemon/adapters/features.js +51 -0
- package/dist/daemon/adapters/registry.js +9 -0
- package/dist/daemon/agent/builtin/agent.md +164 -0
- package/dist/daemon/agent/builtin/load.js +121 -0
- package/dist/daemon/agent/builtin/parse.js +73 -0
- package/dist/daemon/agent/index.js +60 -0
- package/dist/daemon/agent/manifest.js +72 -0
- package/dist/daemon/agent/materialize.js +220 -0
- package/dist/daemon/agent/renderers/claude-code.js +16 -0
- package/dist/daemon/agent/renderers/common.js +61 -0
- package/dist/daemon/agent/renderers/cursor.js +13 -0
- package/dist/daemon/agent/renderers/generic.js +5 -0
- package/dist/daemon/agent/resolve.js +212 -0
- package/dist/daemon/agent/slug.js +27 -0
- package/dist/daemon/agent/types.js +8 -0
- package/dist/daemon/device-store.js +61 -0
- package/dist/daemon/index.js +292 -0
- package/dist/daemon/manage.js +183 -0
- package/dist/daemon/orchestrator.js +400 -0
- package/dist/daemon/pairing.js +93 -0
- package/dist/daemon/paths.js +47 -0
- package/dist/daemon/rpc.js +41 -0
- package/dist/daemon/run-skills.js +148 -0
- package/dist/daemon/services/agent-config.js +1 -0
- package/dist/daemon/services/claude-provider.js +126 -0
- package/dist/daemon/services/cursor-provider.js +94 -0
- package/dist/daemon/services/json-file.js +21 -0
- package/dist/daemon/services/log-buffer.js +43 -0
- package/dist/daemon/services/mcp-secrets.js +36 -0
- package/dist/daemon/session/store.js +137 -0
- package/dist/daemon/transport/ws-client.js +121 -0
- package/dist/pingcode/builtin/SKILL.md +47 -0
- package/dist/pingcode/builtin/reference.md +123 -0
- package/dist/pingcode/cli.js +138 -0
- package/dist/pingcode/config.js +106 -0
- package/dist/pingcode/detect.js +73 -0
- package/dist/pingcode/index.js +8 -0
- package/dist/pingcode/launcher.js +60 -0
- package/dist/pingcode/runtime.js +167 -0
- package/dist/pingcode/skill.js +51 -0
- package/dist/pingcode/token.js +81 -0
- package/package.json +45 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 令牌注入:给会话目录生成一个 `pc` 包装器,令牌只在 exec 时从 0600 文件读入。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不直接往子进程环境里塞 PC_TOKEN?
|
|
4
|
+
// 上游 `pc` 用 clap 的 `env = "PC_TOKEN"` 声明参数,**帮助输出会把环境变量值当默认值
|
|
5
|
+
// 打印出来**(`pc --help` → `[env: PC_TOKEN=真实令牌]`)。agent 一旦执行 --help,
|
|
6
|
+
// 令牌就进了模型上下文,还可能顺着 run 日志回流到平台。
|
|
7
|
+
// 包装器把令牌放在命令行参数里由脚本内部展开,子进程环境里没有 PC_TOKEN,
|
|
8
|
+
// --help 自然打不出任何凭据。(argv 在 `ps` 里可见,单用户开发机可接受。)
|
|
9
|
+
import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { manageDir } from "../daemon/paths.js";
|
|
12
|
+
/** 相对会话目录的 bin 目录(会被 prepend 到 PATH) */
|
|
13
|
+
const BIN_DIR_REL = join(".super-coding", "bin");
|
|
14
|
+
const TOKEN_FILE_REL = join("pingcode", "token");
|
|
15
|
+
/** POSIX 单引号转义:' -> '\'' */
|
|
16
|
+
function shQuote(value) {
|
|
17
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
18
|
+
}
|
|
19
|
+
/** 令牌文件位置(0600);disconnect 时也要清掉,别把有效令牌留在盘上 */
|
|
20
|
+
export function pingcodeTokenFile() {
|
|
21
|
+
return join(manageDir(), TOKEN_FILE_REL);
|
|
22
|
+
}
|
|
23
|
+
export function clearPingcodeToken() {
|
|
24
|
+
const path = pingcodeTokenFile();
|
|
25
|
+
try {
|
|
26
|
+
rmSync(path, { force: true });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function writePingcodeLauncher(opts) {
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
throw new Error("writePingcodeLauncher 只支持 POSIX;Windows 走环境变量注入");
|
|
36
|
+
}
|
|
37
|
+
const tokenDir = join(manageDir(), "pingcode");
|
|
38
|
+
mkdirSync(tokenDir, { recursive: true, mode: 0o700 });
|
|
39
|
+
const tokenFile = pingcodeTokenFile();
|
|
40
|
+
// 每次运行重写:client 凭据换来的令牌会轮换,旧值不能留在盘上。
|
|
41
|
+
writeFileSync(tokenFile, `${opts.token}\n`, { mode: 0o600 });
|
|
42
|
+
chmodSync(tokenFile, 0o600);
|
|
43
|
+
const binDir = join(opts.workdir, BIN_DIR_REL);
|
|
44
|
+
mkdirSync(binDir, { recursive: true, mode: 0o700 });
|
|
45
|
+
const scriptPath = join(binDir, "pc");
|
|
46
|
+
const script = [
|
|
47
|
+
"#!/bin/sh",
|
|
48
|
+
"# 由 scode daemon 生成,请勿修改。",
|
|
49
|
+
"# 令牌只在 exec 时从 0600 文件读取,不进环境变量,避免 `pc --help` 把凭据当默认值打印。",
|
|
50
|
+
`tok=$(cat ${shQuote(tokenFile)} 2>/dev/null) || {`,
|
|
51
|
+
' echo "pc: 缺少 PingCode 令牌文件,请重新运行 scode pingcode connect" >&2',
|
|
52
|
+
" exit 2",
|
|
53
|
+
"}",
|
|
54
|
+
`exec ${shQuote(opts.binary)} --token "$tok" --base-url ${shQuote(opts.baseUrl)} "$@"`,
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
writeFileSync(scriptPath, script, { mode: 0o700 });
|
|
58
|
+
chmodSync(scriptPath, 0o700);
|
|
59
|
+
return { binDir, scriptPath, tokenFile };
|
|
60
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// PingCode 集成的运行时装配:探测 → 凭据 → 令牌注入方式 → 内置 skill。
|
|
2
|
+
//
|
|
3
|
+
// 设计原则:**有则增强,无则静默降级**。任何一步不满足就标记 reason 并返回不可用,
|
|
4
|
+
// 由调用方(orchestrator / CLI status)自行决定提示什么,绝不让 daemon 启动或会话失败。
|
|
5
|
+
import { delimiter } from "node:path";
|
|
6
|
+
import { DEFAULT_BASE_URL, inspectPingcodeConfig } from "./config.js";
|
|
7
|
+
import { detectPingcodeBinary } from "./detect.js";
|
|
8
|
+
import { writePingcodeLauncher } from "./launcher.js";
|
|
9
|
+
import { loadPingcodeSkill } from "./skill.js";
|
|
10
|
+
import { resolveToken, resetTokenCache } from "./token.js";
|
|
11
|
+
/** 配置文件的凭据优先;其次认用户 shell 里已导出的 PC_TOKEN(显式选择,不读 client 凭据对)。 */
|
|
12
|
+
function resolveCredentials(env) {
|
|
13
|
+
const inspected = inspectPingcodeConfig();
|
|
14
|
+
if (inspected.state === "ok") {
|
|
15
|
+
const config = inspected.config;
|
|
16
|
+
return { creds: { config, credential: config.token ? "user_token" : "client_credentials" } };
|
|
17
|
+
}
|
|
18
|
+
// 配置文件坏了但环境里有显式 PC_TOKEN:仍然可用,不必因为一个坏文件把功能关掉
|
|
19
|
+
const envToken = env["PC_TOKEN"]?.trim();
|
|
20
|
+
if (!envToken) {
|
|
21
|
+
return inspected.state === "invalid"
|
|
22
|
+
? { creds: null, invalid: { path: inspected.path, error: inspected.error } }
|
|
23
|
+
: { creds: null };
|
|
24
|
+
}
|
|
25
|
+
const baseUrl = (env["PC_OPEN_API_BASE_URL"]?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
26
|
+
return { creds: { config: { base_url: /^https?:\/\//i.test(baseUrl) ? baseUrl : DEFAULT_BASE_URL, token: envToken }, credential: "env_token" } };
|
|
27
|
+
}
|
|
28
|
+
export class PingcodeBridge {
|
|
29
|
+
opts;
|
|
30
|
+
state;
|
|
31
|
+
injection;
|
|
32
|
+
constructor(opts, state, injection) {
|
|
33
|
+
this.opts = opts;
|
|
34
|
+
this.state = state;
|
|
35
|
+
this.injection = injection;
|
|
36
|
+
}
|
|
37
|
+
static async create(options = {}) {
|
|
38
|
+
const env = options.env ?? process.env;
|
|
39
|
+
const injection = process.platform === "win32" ? "env" : "launcher";
|
|
40
|
+
const opts = { env, token: options.token ?? {} };
|
|
41
|
+
if (env["SC_PINGCODE"] === "off") {
|
|
42
|
+
return new PingcodeBridge(opts, { reason: "disabled", detail: "SC_PINGCODE=off" }, injection);
|
|
43
|
+
}
|
|
44
|
+
// 先探二进制:它决定 status 能报出多少信息,且与凭据是否就绪无关
|
|
45
|
+
const binary = await detectPingcodeBinary(env);
|
|
46
|
+
if (!binary) {
|
|
47
|
+
return new PingcodeBridge(opts, { reason: "binary_missing" }, injection);
|
|
48
|
+
}
|
|
49
|
+
const { creds, invalid } = resolveCredentials(env);
|
|
50
|
+
if (!creds) {
|
|
51
|
+
return new PingcodeBridge(opts, invalid
|
|
52
|
+
? { binary, reason: "config_invalid", detail: `${invalid.path}:${invalid.error}` }
|
|
53
|
+
: { binary, reason: "no_credentials" }, injection);
|
|
54
|
+
}
|
|
55
|
+
const skill = loadPingcodeSkill();
|
|
56
|
+
if (!skill) {
|
|
57
|
+
return new PingcodeBridge(opts, { binary, config: creds.config, credential: creds.credential, reason: "skill_missing" }, injection);
|
|
58
|
+
}
|
|
59
|
+
return new PingcodeBridge(opts, { binary, config: creds.config, credential: creds.credential, skill }, injection);
|
|
60
|
+
}
|
|
61
|
+
get available() {
|
|
62
|
+
return this.state.reason === undefined && this.state.config !== undefined;
|
|
63
|
+
}
|
|
64
|
+
/** 内置 skill(不可用时为 null) */
|
|
65
|
+
skill() {
|
|
66
|
+
return this.state.skill ?? null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 按需重探:配置常常是 daemon 起来之后才写入/改写的(`scode pingcode connect`),
|
|
70
|
+
* 不重探就得让用户重启 daemon 才生效。探测有缓存,代价只是一次小文件读。
|
|
71
|
+
*/
|
|
72
|
+
async refresh() {
|
|
73
|
+
if (this.state.reason === "disabled")
|
|
74
|
+
return;
|
|
75
|
+
const binary = this.state.binary ?? (await detectPingcodeBinary(this.opts.env));
|
|
76
|
+
const { creds, invalid } = resolveCredentials(this.opts.env);
|
|
77
|
+
const skill = this.state.skill ?? loadPingcodeSkill();
|
|
78
|
+
// 凭据可能在运行期被 connect 改写(含从 access token 切到 client 凭据)
|
|
79
|
+
const changed = JSON.stringify(creds?.config ?? null) !== JSON.stringify(this.state.config ?? null);
|
|
80
|
+
if (binary)
|
|
81
|
+
this.state.binary = binary;
|
|
82
|
+
if (creds) {
|
|
83
|
+
this.state.config = creds.config;
|
|
84
|
+
this.state.credential = creds.credential;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
delete this.state.config;
|
|
88
|
+
delete this.state.credential;
|
|
89
|
+
}
|
|
90
|
+
if (skill)
|
|
91
|
+
this.state.skill = skill;
|
|
92
|
+
if (changed)
|
|
93
|
+
resetTokenCache();
|
|
94
|
+
if (!binary) {
|
|
95
|
+
this.state.reason = "binary_missing";
|
|
96
|
+
delete this.state.detail;
|
|
97
|
+
}
|
|
98
|
+
else if (!creds) {
|
|
99
|
+
this.state.reason = invalid ? "config_invalid" : "no_credentials";
|
|
100
|
+
if (invalid)
|
|
101
|
+
this.state.detail = `${invalid.path}:${invalid.error}`;
|
|
102
|
+
else
|
|
103
|
+
delete this.state.detail;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
else if (!skill) {
|
|
107
|
+
this.state.reason = "skill_missing";
|
|
108
|
+
delete this.state.detail;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
delete this.state.reason;
|
|
112
|
+
delete this.state.detail;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 一次运行所需的全部注入物。不可用返回 null。
|
|
117
|
+
* 令牌换取失败会抛错,由调用方降级(不影响会话本身)。
|
|
118
|
+
*/
|
|
119
|
+
async prepareRun(workdir) {
|
|
120
|
+
await this.refresh();
|
|
121
|
+
const config = this.state.config;
|
|
122
|
+
const binary = this.state.binary;
|
|
123
|
+
const skill = this.state.skill;
|
|
124
|
+
if (!config || !binary || !skill)
|
|
125
|
+
return null;
|
|
126
|
+
const token = await resolveToken(config, this.opts.token);
|
|
127
|
+
const base = config.base_url;
|
|
128
|
+
// 中和继承来的凭据:子进程环境里绝不允许出现真实凭据(`pc --help` 会回显环境变量值)
|
|
129
|
+
const neutral = {
|
|
130
|
+
PC_TOKEN: "",
|
|
131
|
+
PC_CLIENT_ID: "",
|
|
132
|
+
PC_CLIENT_SECRET: "",
|
|
133
|
+
PC_OPEN_API_BASE_URL: base,
|
|
134
|
+
};
|
|
135
|
+
if (this.injection === "launcher") {
|
|
136
|
+
const { binDir } = writePingcodeLauncher({
|
|
137
|
+
workdir,
|
|
138
|
+
binary: binary.path,
|
|
139
|
+
baseUrl: base,
|
|
140
|
+
token: token.token,
|
|
141
|
+
});
|
|
142
|
+
const parentPath = this.opts.env["PATH"] ?? "";
|
|
143
|
+
return {
|
|
144
|
+
env: { ...neutral, PATH: parentPath ? `${binDir}${delimiter}${parentPath}` : binDir },
|
|
145
|
+
skill,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Windows 兜底:没有可靠的 sh 包装器。已知取舍——`pc --help` 会把 PC_TOKEN 打出来,
|
|
149
|
+
// skill 里已明令禁止执行 --help。
|
|
150
|
+
return { env: { ...neutral, PC_TOKEN: token.token }, skill };
|
|
151
|
+
}
|
|
152
|
+
status() {
|
|
153
|
+
return {
|
|
154
|
+
available: this.available,
|
|
155
|
+
...(this.state.reason ? { reason: this.state.reason } : {}),
|
|
156
|
+
...(this.state.detail ? { detail: this.state.detail } : {}),
|
|
157
|
+
...(this.state.binary ? { binary: this.state.binary } : {}),
|
|
158
|
+
...(this.state.config ? { base_url: this.state.config.base_url } : {}),
|
|
159
|
+
...(this.state.credential ? { credential: this.state.credential } : {}),
|
|
160
|
+
injection: this.injection,
|
|
161
|
+
...(this.state.skill ? { skill: this.state.skill.name } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function createPingcodeBridge(options = {}) {
|
|
166
|
+
return PingcodeBridge.create(options);
|
|
167
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 内置 PingCode skill:随 CLI 包分发,物化进会话的 skills 目录。
|
|
2
|
+
// 读不到文件时返回 null(daemon 降级为「不注入 pc」),而不是让会话失败。
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
/** dist 与 src 同构:builtin/ 与 skill.js 同目录(构建期由 copy-assets 拷贝) */
|
|
6
|
+
const SKILL_PATH = fileURLToPath(new URL("./builtin/SKILL.md", import.meta.url));
|
|
7
|
+
const REFERENCE_PATH = fileURLToPath(new URL("./builtin/reference.md", import.meta.url));
|
|
8
|
+
/** 正文过短视为打包缺失/截断,宁可不注入 */
|
|
9
|
+
const MIN_SKILL_CHARS = 200;
|
|
10
|
+
export const PINGCODE_SKILL_NAME = "use-pingcode-cli";
|
|
11
|
+
const SKILL_ID = "builtin:use-pingcode-cli";
|
|
12
|
+
let cached;
|
|
13
|
+
function stripBom(text) {
|
|
14
|
+
return text.replace(/^\uFEFF/, "");
|
|
15
|
+
}
|
|
16
|
+
function frontmatterDescription(content) {
|
|
17
|
+
const block = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
const value = block?.[1]?.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
19
|
+
return value ? value.replace(/^["']|["']$/g, "") : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function loadPingcodeSkill() {
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
try {
|
|
25
|
+
const content = stripBom(readFileSync(SKILL_PATH, "utf8"));
|
|
26
|
+
const reference = stripBom(readFileSync(REFERENCE_PATH, "utf8"));
|
|
27
|
+
if (!/^---\r?\n/.test(content))
|
|
28
|
+
throw new Error("SKILL.md 缺少 frontmatter");
|
|
29
|
+
if (content.trim().length < MIN_SKILL_CHARS)
|
|
30
|
+
throw new Error(`SKILL.md 正文过短(<${MIN_SKILL_CHARS} 字符),疑似打包缺失`);
|
|
31
|
+
if (reference.trim().length < MIN_SKILL_CHARS)
|
|
32
|
+
throw new Error("reference.md 内容过短,疑似打包缺失");
|
|
33
|
+
cached = {
|
|
34
|
+
id: SKILL_ID,
|
|
35
|
+
name: PINGCODE_SKILL_NAME,
|
|
36
|
+
...(frontmatterDescription(content) ? { description: frontmatterDescription(content) } : {}),
|
|
37
|
+
content,
|
|
38
|
+
files: [{ path: "reference.md", content: reference }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error(`[daemon] 内置 PingCode skill 不可用(${SKILL_PATH}):${err.message}`);
|
|
43
|
+
cached = null;
|
|
44
|
+
}
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
/** 测试用:清空缓存 */
|
|
48
|
+
export function resetPingcodeSkillCache() {
|
|
49
|
+
cached = undefined;
|
|
50
|
+
}
|
|
51
|
+
export const pingcodeSkillPaths = { SKILL_PATH, REFERENCE_PATH };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
2
|
+
const DEFAULT_TTL_SECONDS = 3_600;
|
|
3
|
+
/** 提前 60 秒过期,避免边界上拿着刚失效的令牌去发业务请求 */
|
|
4
|
+
const EXPIRY_MARGIN_MS = 60_000;
|
|
5
|
+
let cache;
|
|
6
|
+
export function resetTokenCache() {
|
|
7
|
+
cache = undefined;
|
|
8
|
+
}
|
|
9
|
+
function cacheKey(config) {
|
|
10
|
+
return `${config.base_url}|${config.client_id ?? config.token ?? ""}`;
|
|
11
|
+
}
|
|
12
|
+
export async function resolveToken(config, opts = {}) {
|
|
13
|
+
if (config.token)
|
|
14
|
+
return { token: config.token, source: "config" };
|
|
15
|
+
const now = opts.now ?? Date.now;
|
|
16
|
+
const key = cacheKey(config);
|
|
17
|
+
if (cache && cache.key === key && cache.expires_at > now()) {
|
|
18
|
+
return { token: cache.token, source: "client_credentials", expires_at: cache.expires_at };
|
|
19
|
+
}
|
|
20
|
+
const clientId = config.client_id;
|
|
21
|
+
const clientSecret = config.client_secret;
|
|
22
|
+
if (!clientId || !clientSecret) {
|
|
23
|
+
throw new Error("PingCode 令牌换取失败:缺少 client_id / client_secret");
|
|
24
|
+
}
|
|
25
|
+
const query = new URLSearchParams({
|
|
26
|
+
grant_type: "client_credentials",
|
|
27
|
+
client_id: clientId,
|
|
28
|
+
client_secret: clientSecret,
|
|
29
|
+
});
|
|
30
|
+
const url = `${config.base_url}/v1/auth/token?${query.toString()}`;
|
|
31
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetcher(url, { method: "GET", signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// 不拼原始错误消息:它可能包含带 client_secret 的完整 URL。
|
|
38
|
+
const name = err?.name ?? "Error";
|
|
39
|
+
throw new Error(`PingCode 令牌换取失败:${name === "TimeoutError" ? "请求超时" : name}`);
|
|
40
|
+
}
|
|
41
|
+
const body = await res.text();
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`PingCode 令牌换取失败:HTTP ${res.status}${body ? `(${truncate(body)})` : ""}`);
|
|
44
|
+
}
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(body);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error("PingCode 令牌换取失败:响应不是 JSON");
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed.access_token !== "string" || !parsed.access_token) {
|
|
53
|
+
throw new Error("PingCode 令牌换取失败:响应缺少 access_token");
|
|
54
|
+
}
|
|
55
|
+
const expiresAt = expiryOf(parsed.expires_in, now());
|
|
56
|
+
cache = { key, token: parsed.access_token, expires_at: expiresAt };
|
|
57
|
+
return { token: parsed.access_token, source: "client_credentials", expires_at: expiresAt };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `expires_in` 的语义在服务端并不统一:公有云返回 TTL 秒数,而 RC 环境实测返回的是
|
|
61
|
+
* **绝对过期时间戳**(epoch 秒,如 1795060115 = 2026-11-19)。
|
|
62
|
+
* 若一律当 TTL,会缓存成 56 年,令牌真过期后一直拿旧值去请求、只在业务接口上收到 401,
|
|
63
|
+
* 且永远不会触发刷新。这里按量级区分:超过 10 年的一律视为时间戳。
|
|
64
|
+
*/
|
|
65
|
+
const MAX_PLAUSIBLE_TTL_SECONDS = 10 * 365 * 24 * 3_600;
|
|
66
|
+
function expiryOf(raw, nowMs) {
|
|
67
|
+
const at = (base) => Math.max(base - EXPIRY_MARGIN_MS, nowMs + 30_000);
|
|
68
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
|
|
69
|
+
return at(nowMs + DEFAULT_TTL_SECONDS * 1_000);
|
|
70
|
+
}
|
|
71
|
+
if (raw > MAX_PLAUSIBLE_TTL_SECONDS) {
|
|
72
|
+
const absolute = raw * 1_000;
|
|
73
|
+
// 已经是过去的时刻(时钟偏差/服务端异常)就退回保守 TTL,避免无限复用一个死令牌
|
|
74
|
+
return absolute > nowMs ? at(absolute) : at(nowMs + DEFAULT_TTL_SECONDS * 1_000);
|
|
75
|
+
}
|
|
76
|
+
return at(nowMs + raw * 1_000);
|
|
77
|
+
}
|
|
78
|
+
function truncate(text) {
|
|
79
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
80
|
+
return flat.length > 200 ? `${flat.slice(0, 200)}…` : flat;
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-coding/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Super Coding CLI:把本机 Claude Code / Cursor 接入远程控制台(scode 命令与后台 daemon)",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"scode": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/cli.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"super-coding",
|
|
22
|
+
"agent",
|
|
23
|
+
"daemon",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cursor",
|
|
26
|
+
"remote"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.10.1",
|
|
30
|
+
"@types/ws": "^8.18.1",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^3.2.7"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^14.0.2",
|
|
37
|
+
"ws": "^8.21.3",
|
|
38
|
+
"@super-coding/protocol": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "tsx watch src/cli.ts",
|
|
42
|
+
"build": "pnpm --filter @super-coding/protocol build && rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
|
|
43
|
+
"link:local": "node scripts/link-local.mjs",
|
|
44
|
+
"start": "node dist/cli.js daemon run",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
}
|
|
6
48
|
}
|