@social-data/social-neuron-user-query 0.0.0-stage → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // ⚠️ 本文件由 core/cli/gen.py 生成,请勿手改;要改改 gen.py 的 _CLI_JS 后重跑 gen。
5
+ //
6
+ // 能力 CLI —— 通用薄运行时(纯 JS,零依赖):一能力一命令、一包、一配置文件,互不影响。
7
+ // <cmd> <action> [--flags] 调用一条命令
8
+ // <cmd> [-h] / <cmd> <action> -h 帮助
9
+ // <cmd> bind --api-url <url> --token <token> 写 ~/.social-neuron/<key>_config.json
10
+ // <cmd> bind --logout 删除该配置文件
11
+ // <cmd> skills install [dir] 覆盖写入 SKILL.md
12
+ // 能力 key / 命令名全部来自同包 manifest.json(见 CAPABILITY / COMMAND);换能力只改 manifest。
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+
18
+ const MANIFEST = require(path.join(__dirname, "..", "manifest.json"));
19
+ const VERSION = require(path.join(__dirname, "..", "package.json")).version;
20
+ const CAPABILITY = MANIFEST.capability; // ← 能力 key(来自 manifest)
21
+ const COMMAND = MANIFEST.command || ("social-neuron-" + CAPABILITY); // ← 命令名(来自 manifest)
22
+
23
+ const EXIT = { auth_error: 3, validation: 2, config: 2, network: 4, outdated: 2 };
24
+
25
+ // 配置:一能力一文件 ~/.social-neuron/<key>_config.json,扁平 {api_url, token}(不区分环境)。
26
+ function configPath() {
27
+ const dir = process.env.NEURON_CONFIG_DIR || path.join(os.homedir(), ".social-neuron");
28
+ return path.join(dir, CAPABILITY + "_config.json");
29
+ }
30
+ function resolveConfig() {
31
+ let api = process.env.NEURON_API_BASE_URL || "";
32
+ let token = process.env.NEURON_TOKEN || "";
33
+ try {
34
+ const cfg = JSON.parse(fs.readFileSync(configPath(), "utf8"));
35
+ api = api || cfg.api_url || "";
36
+ token = token || cfg.token || "";
37
+ } catch (_e) { /* 无配置文件:仅用 env */ }
38
+ return { api: api.replace(/\/+$/, ""), token };
39
+ }
40
+
41
+ function emit(env) {
42
+ process.stdout.write(JSON.stringify(env) + "\n");
43
+ const dataExit = env.ok && env.data && env.data.meta && env.data.meta.exit_code;
44
+ const status = env.ok
45
+ ? ([0, 1, 2].includes(dataExit) ? dataExit : 0)
46
+ : (EXIT[env.error && env.error.type] ?? 1);
47
+ process.exit(status);
48
+ }
49
+ function fail(type, code, message, extra) {
50
+ emit({ ok: false, error: { type, code, message, ...(extra || {}) } });
51
+ }
52
+
53
+ // 网关统一 {code,msg,data}(code=0 成功)→ 归一化 {ok,data|error}。
54
+ function forgeType(code) {
55
+ const s = String(code);
56
+ if (s.startsWith("401") || s.startsWith("403")) return "auth_error";
57
+ if (s.startsWith("426")) return "outdated";
58
+ if (s.startsWith("422") || s.startsWith("400")) return "validation";
59
+ return "api_error";
60
+ }
61
+ function normalize(body) {
62
+ if (body && typeof body.ok === "boolean") return body;
63
+ if (body && Object.prototype.hasOwnProperty.call(body, "code")) {
64
+ if (body.code === 0) return { ok: true, data: body.data };
65
+ const type = forgeType(body.code);
66
+ let message = body.msg || "";
67
+ if (type === "outdated") message += "(升级后请重新加载 SKILL.md 与 manifest 再调用)";
68
+ // 网关在 data 里带审计 trace_id 的业务错误(如查询失败):透出供 get-query 回查。
69
+ const extra = body.data && body.data.trace_id ? { trace_id: body.data.trace_id } : {};
70
+ return { ok: false, error: { type, code: body.code, message, ...extra } };
71
+ }
72
+ return { ok: false, error: { type: "api_error", code: "UNKNOWN", message: JSON.stringify(body).slice(0, 200) } };
73
+ }
74
+
75
+ function findCmd(action) { return MANIFEST.tools.find((t) => t.action === action); }
76
+ function _flag(a, name) { const i = a.indexOf(name); return i >= 0 ? a[i + 1] : undefined; }
77
+
78
+ // bind:只写本能力自己的配置文件(原子写 + 0600);--logout 删该文件。互不影响其它能力。
79
+ function doBind(a) {
80
+ const cfgPath = configPath();
81
+ if (a.includes("--logout")) {
82
+ try { fs.unlinkSync(cfgPath); process.stderr.write("[" + COMMAND + "] 已清除配置 " + cfgPath + "\n"); }
83
+ catch (_e) { process.stderr.write("[" + COMMAND + "] 无配置可清除\n"); }
84
+ return;
85
+ }
86
+ const api = _flag(a, "--api-url");
87
+ const token = _flag(a, "--token");
88
+ if (!api || !token) {
89
+ return fail("validation", "BIND_ARGS", "bind 需要 --api-url 与 --token", {
90
+ usage: { example: COMMAND + " bind --api-url http://host/api/v2 --token <token>" },
91
+ });
92
+ }
93
+ const cfg = { api_url: api.replace(/\/+$/, ""), token };
94
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
95
+ const tmp = cfgPath + ".tmp";
96
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2), { mode: 0o600 });
97
+ fs.renameSync(tmp, cfgPath); // 原子替换
98
+ process.stderr.write("[" + COMMAND + "] 已绑定 → " + cfg.api_url + " (" + cfgPath + ")\n");
99
+ printWelcome();
100
+ }
101
+
102
+ function skillsDir() { return process.env.OPENCLAW_SKILLS_DIR || path.join(os.homedir(), ".openclaw", "skills"); }
103
+ function printWelcome() {
104
+ const w = (s) => process.stderr.write(s + "\n");
105
+ const skill = MANIFEST.skill;
106
+ const dir = skillsDir();
107
+ w("");
108
+ w("──────────────────────────────────────────────");
109
+ w(" " + COMMAND + " v" + VERSION + " · 已就绪(能力:" + CAPABILITY + ")");
110
+ w(" 命令:");
111
+ for (const t of MANIFEST.tools) w(" " + COMMAND + " " + t.action + " — " + t.summary);
112
+ if (skill) { w(""); w(" Agent 需加载 SKILL.md:"); w(" " + path.join(dir, skill.slug, "SKILL.md")); }
113
+ w("");
114
+ w(" 下一步:" + COMMAND + " <action> -h 查看入参;" + COMMAND + " skills install 重写技能文件");
115
+ w("──────────────────────────────────────────────");
116
+ }
117
+
118
+ // ── 嵌套对象形状:object(json) / list[object](json) 这类 flag 光看类型名没法拼 JSON,
119
+ // 这里把 input_schema.$defs 里**被引用到的**对象摊开印出来(与 SKILL.md 同源同内容)。
120
+ // 没有这段时,调用方只能靠 422 报错反推字段名。
121
+ function typeStr(node) {
122
+ if (!node || typeof node !== "object") return "any";
123
+ if (typeof node.$ref === "string") return node.$ref.split("/").pop();
124
+ const union = node.anyOf || node.oneOf;
125
+ if (union) {
126
+ const parts = union.filter((n) => n && n.type !== "null").map(typeStr);
127
+ return parts.length ? parts.join("|") : "any";
128
+ }
129
+ if (Array.isArray(node.enum)) return "enum(" + node.enum.join("|") + ")";
130
+ if (node.type === "array") return "list[" + typeStr(node.items) + "]";
131
+ return node.type || "any";
132
+ }
133
+ function refsOf(node, acc) {
134
+ if (!node || typeof node !== "object") return acc;
135
+ if (Array.isArray(node)) { for (const n of node) refsOf(n, acc); return acc; }
136
+ if (typeof node.$ref === "string") acc.add(node.$ref.split("/").pop());
137
+ for (const k of Object.keys(node)) if (k !== "$defs") refsOf(node[k], acc);
138
+ return acc;
139
+ }
140
+ function objectBlocks(schema) {
141
+ const defs = (schema && schema.$defs) || {};
142
+ const queue = [...refsOf((schema && schema.properties) || {}, new Set())], seen = new Set();
143
+ while (queue.length) { // 递归:GroupLevel 会引用自己、Leaf 会被 AndNode 引用
144
+ const name = queue.shift();
145
+ if (seen.has(name) || !defs[name]) continue;
146
+ seen.add(name);
147
+ for (const r of refsOf(defs[name], new Set())) queue.push(r);
148
+ }
149
+ const out = [];
150
+ for (const name of [...seen].sort()) {
151
+ const d = defs[name], props = d.properties;
152
+ if (!props) continue;
153
+ const required = new Set(d.required || []);
154
+ const rows = Object.keys(props).map((k) => [k, typeStr(props[k]), required.has(k) ? "required" : "optional",
155
+ props[k].description || ""]);
156
+ const w0 = Math.max(...rows.map((r) => r[0].length)), w1 = Math.max(...rows.map((r) => r[1].length));
157
+ out.push("对象 " + name + ":" + (d.description ? " " + d.description.split("\n")[0] : ""));
158
+ for (const r of rows) out.push(" " + r[0].padEnd(w0) + " " + r[1].padEnd(w1) + " " + r[2].padEnd(8) + " " + r[3]);
159
+ }
160
+ return out;
161
+ }
162
+
163
+ function usageOf(cmd) {
164
+ const line = (f) => " " + f.flag + " " + f.type + (f.required ? " required" : " optional") + " " + (f.desc || "");
165
+ const req = cmd.flags.filter((f) => f.required), opt = cmd.flags.filter((f) => !f.required);
166
+ const blocks = objectBlocks(cmd.input_schema);
167
+ return {
168
+ command: COMMAND + " " + cmd.action,
169
+ required: req.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
170
+ optional: opt.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
171
+ objects: blocks, // 同样带进 INVALID_ARGS 报错里,拼错时直接能看到形状
172
+ example: (COMMAND + " " + cmd.action + " " + req.map((f) => f.flag + " <v>").join(" ")).trim(),
173
+ _text: [COMMAND + " " + cmd.action + " — " + cmd.summary, "Inputs:", ...cmd.flags.map(line),
174
+ ...(blocks.length ? ["", ...blocks] : [])].join("\n"),
175
+ };
176
+ }
177
+ function printHelp(action) {
178
+ const w = (s) => process.stderr.write(s + "\n");
179
+ if (action) {
180
+ const c = findCmd(action);
181
+ if (!c) return fail("validation", "UNKNOWN_COMMAND", "未知命令 " + action);
182
+ const u = usageOf(c); w(u._text); w(""); w("示例:" + u.example); return process.exit(0);
183
+ }
184
+ w(COMMAND + " v" + VERSION + " — Social Neuron 能力 CLI(能力:" + CAPABILITY + ";供 AI agent 调用)");
185
+ w("");
186
+ w("用法:" + COMMAND + " <action> [--flags]");
187
+ w(" 标量 --flag v;数组/对象一律 JSON:--flag '[\"a\",\"b\"]' / --flag '{\"k\":\"v\"}';写/删/触发加 -y");
188
+ w("");
189
+ w("命令:");
190
+ for (const c of MANIFEST.tools) w(" " + COMMAND + " " + c.action + " — " + c.summary);
191
+ w("");
192
+ w("其它:");
193
+ w(" " + COMMAND + " <action> -h 查看某命令完整入参/出参");
194
+ w(" " + COMMAND + " bind --api-url <url> --token <t> 绑定网关+令牌(写 " + configPath() + ")");
195
+ w(" " + COMMAND + " bind --logout 删除本能力配置");
196
+ w(" " + COMMAND + " skills install [dir] 覆盖写入 SKILL.md 到 ~/.openclaw/skills");
197
+ w(" " + COMMAND + " --version");
198
+ w("");
199
+ w("鉴权/网关:env NEURON_API_BASE_URL / NEURON_TOKEN 覆盖配置;令牌走头 X-Social-Neuron-CLI-Key。");
200
+ process.exit(0);
201
+ }
202
+
203
+ function parseArgs(cmd, argv) {
204
+ const byFlag = Object.fromEntries(cmd.flags.map((f) => [f.flag, f]));
205
+ const args = {};
206
+ for (let i = 0; i < argv.length; i++) {
207
+ const a = argv[i];
208
+ if (a === "-y") continue;
209
+ if (!a.startsWith("--")) continue;
210
+ const f = byFlag[a];
211
+ if (!f) return { err: fail("validation", "UNKNOWN_FLAG", "未知参数 " + a) };
212
+ const raw = argv[++i];
213
+ if (raw === undefined) return { err: fail("validation", "MISSING_VALUE", a + " 缺少取值") };
214
+ let val = raw;
215
+ if (f.type.indexOf("(json)") >= 0) {
216
+ // 数组/对象一律按 JSON 解析(与 MCP 入参同形)。不做「逗号分隔」这类特例:
217
+ // 同一份文档要能同时用于两个出口,取值写法就只能有一种。
218
+ try { val = JSON.parse(raw); } catch {
219
+ const hint = f.type.startsWith("list") ? "JSON 数组,如 '[\"a\",\"b\"]'" : "JSON 对象,如 '{\"k\":\"v\"}'";
220
+ return { err: fail("validation", "BAD_JSON", a + " 需为 " + hint + "(当前取值:" + raw + ")") };
221
+ }
222
+ if (f.type.startsWith("list") && !Array.isArray(val)) {
223
+ return { err: fail("validation", "BAD_JSON", a + " 需为 JSON 数组,如 '[\"a\",\"b\"]';单个取值也要写成数组") };
224
+ }
225
+ }
226
+ else if (f.type === "integer") val = Number(raw);
227
+ args[f.field] = val;
228
+ }
229
+ return { args };
230
+ }
231
+
232
+ async function main() {
233
+ let argv = process.argv.slice(2);
234
+ if (argv.length === 0 && MANIFEST.default_action) argv = [MANIFEST.default_action];
235
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") return printHelp();
236
+ if (argv[0] === "--version" || argv[0] === "-V") { process.stdout.write(VERSION + "\n"); return; }
237
+ if (argv[0] === "bind") return doBind(argv.slice(1));
238
+ if (argv[0] === "skills" && argv[1] === "install") { require(path.join(__dirname, "install-skills.js")).installTo(argv[2]); return; }
239
+
240
+ const [action, ...rest] = argv;
241
+ if (rest.includes("--help") || rest.includes("-h")) return printHelp(action);
242
+
243
+ const cmd = findCmd(action);
244
+ if (!cmd) return fail("validation", "UNKNOWN_COMMAND", "未知命令 " + action);
245
+
246
+ const parsed = parseArgs(cmd, rest);
247
+ if (parsed.err) return;
248
+ const { args } = parsed;
249
+ const missing = cmd.flags.filter((f) => f.required && args[f.field] === undefined).map((f) => f.flag);
250
+ if (missing.length) return fail("validation", "INVALID_ARGS", "缺少必填参数:" + missing.join(" "), { usage: usageOf(cmd) });
251
+
252
+ const { api, token } = resolveConfig();
253
+ if (!api) return fail("config", "CONFIG", "未配置网关地址(" + COMMAND + " bind 或 env NEURON_API_BASE_URL)");
254
+ if (!token) return fail("auth_error", "AUTH_MISSING", "未配置 token(" + COMMAND + " bind 或 env NEURON_TOKEN)");
255
+
256
+ try {
257
+ // 直达能力级出口 /{key}/{op};body 即该 op 的入参对象(send=args)。
258
+ const res = await fetch(api + cmd.transport.path, {
259
+ method: cmd.transport.method,
260
+ headers: {
261
+ "X-Social-Neuron-CLI-Key": token,
262
+ "Content-Type": "application/json",
263
+ "X-Caller": "ai-agent",
264
+ "X-Neuron-CLI-Version": VERSION,
265
+ "X-Neuron-Manifest-Rev": String(MANIFEST.revision),
266
+ },
267
+ body: JSON.stringify(args),
268
+ signal: AbortSignal.timeout(30000),
269
+ });
270
+ const text = await res.text();
271
+ let body;
272
+ try { body = JSON.parse(text); } catch { return fail("api_error", "HTTP_" + res.status, text.slice(0, 200)); }
273
+ emit(normalize(body));
274
+ } catch (e) { fail("network", "NETWORK", "连接网关失败: " + e.message); }
275
+ }
276
+
277
+ main();
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // ⚠️ 本文件由 core/cli/gen.py 生成,请勿手改。
5
+ // postinstall:把包内 SKILL.md 覆盖写到 OpenClaw 技能目录(OPENCLAW_SKILLS_DIR 或 ~/.openclaw/skills)。
6
+
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const path = require("path");
10
+
11
+ function installTo(dir) {
12
+ const dst = dir || process.env.OPENCLAW_SKILLS_DIR || path.join(os.homedir(), ".openclaw", "skills");
13
+ const src = path.join(__dirname, "..", "skills");
14
+ if (!fs.existsSync(src)) { console.error("[neuron] no bundled skills; skip"); return; }
15
+ const written = [];
16
+ for (const name of fs.readdirSync(src)) {
17
+ const from = path.join(src, name);
18
+ if (!fs.statSync(from).isDirectory()) continue;
19
+ const to = path.join(dst, name);
20
+ fs.mkdirSync(to, { recursive: true });
21
+ for (const f of fs.readdirSync(from)) { fs.copyFileSync(path.join(from, f), path.join(to, f)); written.push(path.join(to, f)); }
22
+ }
23
+ console.error("[neuron] skills → " + dst + " (" + written.length + " files)");
24
+ for (const f of written.filter((p) => p.endsWith(".md"))) console.error("[neuron] 加载:" + f);
25
+ }
26
+
27
+ module.exports = { installTo };
28
+
29
+ if (require.main === module) {
30
+ try { installTo(); } catch (e) { console.error("[neuron] install-skills skipped:", e.message); }
31
+ }