@jeffusion/tianshu 0.0.0-stage → 0.1.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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import {main} from '../cli/main.mjs';
3
+ main().catch(error=>{console.error(JSON.stringify({ok:false,error:error.message}));process.exitCode=1;});
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "tianshu",
3
+ "version": "0.1.1",
4
+ "build_id": "e2f64d01ba77022f91e4"
5
+ }
package/cli/main.mjs ADDED
@@ -0,0 +1,140 @@
1
+ // cli/main.mjs
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, lstatSync } from "node:fs";
3
+ import { join as join2, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execFileSync } from "node:child_process";
6
+
7
+ // cli/probe.mjs
8
+ import { spawn } from "node:child_process";
9
+ function probe(server, python, config) {
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(process.execPath, [server], { env: { ...process.env, TIANSHU_PYTHON: python, TIANSHU_CONFIG: config, PYTHONDONTWRITEBYTECODE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
12
+ let buffer = "", bytes = 0, finished = false;
13
+ const timer = setTimeout(() => finish(new Error("MCP health check timed out")), 12e3);
14
+ function finish(error, value) {
15
+ if (finished) return;
16
+ finished = true;
17
+ clearTimeout(timer);
18
+ child.kill();
19
+ const force = setTimeout(() => child.kill("SIGKILL"), 1e3);
20
+ force.unref();
21
+ child.once("close", () => clearTimeout(force));
22
+ error ? reject(error) : resolve(value);
23
+ }
24
+ const send = (value) => child.stdin.write(JSON.stringify(value) + "\n");
25
+ child.on("error", (error) => finish(error));
26
+ child.on("exit", () => {
27
+ if (!finished) finish(new Error("MCP server exited before health check completed"));
28
+ });
29
+ child.stdin.on("error", (error) => finish(error));
30
+ child.stderr.resume();
31
+ child.stdout.on("data", (chunk) => {
32
+ bytes += chunk.length;
33
+ if (bytes > 4 * 1024 * 1024) return finish(new Error("MCP response exceeds health check limit"));
34
+ buffer += chunk.toString();
35
+ let newline;
36
+ while ((newline = buffer.indexOf("\n")) >= 0) {
37
+ const line = buffer.slice(0, newline);
38
+ buffer = buffer.slice(newline + 1);
39
+ if (!line.trim()) continue;
40
+ try {
41
+ const response = JSON.parse(line);
42
+ if (response.error) throw new Error("MCP health request rejected");
43
+ if (response.id === 1) {
44
+ if (response.result?.serverInfo?.name !== "tianshu") throw new Error("Unexpected MCP server identity");
45
+ send({ jsonrpc: "2.0", method: "notifications/initialized" });
46
+ send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
47
+ } else if (response.id === 2) {
48
+ const tools = (response.result?.tools || []).map((t) => t.name).sort();
49
+ if (JSON.stringify(tools) !== JSON.stringify(["delete_task", "get_task", "list_tasks", "open_workspace"])) throw new Error("Unexpected MCP tool inventory");
50
+ send({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "list_tasks", arguments: {} } });
51
+ } else if (response.id === 3) {
52
+ const data = response.result?.structuredContent;
53
+ if (response.result?.isError || !data?.readonly || !data.sources?.length || data.sources.some((s) => !s.ok)) throw new Error("Configured ledger is not healthy/readable");
54
+ finish(null, { build: data.build, task_count: data.tasks.length, tools: 4 });
55
+ }
56
+ } catch (error) {
57
+ finish(error);
58
+ }
59
+ }
60
+ });
61
+ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "tianshu-doctor", version: "1" } } });
62
+ });
63
+ }
64
+
65
+ // src/contracts/config-path.mjs
66
+ import { homedir } from "node:os";
67
+ import { isAbsolute, join } from "node:path";
68
+ function dataHome() {
69
+ return process.env.TIANSHU_HOME || (process.platform === "darwin" ? join(homedir(), "Library/Application Support/Tianshu") : join(process.env.XDG_DATA_HOME || join(homedir(), ".local/share"), "tianshu"));
70
+ }
71
+ function configPath(explicit) {
72
+ const path = explicit || process.env.TIANSHU_CONFIG || join(dataHome(), "config.json");
73
+ if (!isAbsolute(path)) throw new Error("Config path must be absolute");
74
+ return path;
75
+ }
76
+
77
+ // cli/main.mjs
78
+ var packageRoot = fileURLToPath(new URL("../", import.meta.url));
79
+ var pkg = JSON.parse(readFileSync(join2(packageRoot, "package.json"), "utf8"));
80
+ var print = (value) => console.log(JSON.stringify(value, null, 2));
81
+ var help = `\u5929\u67A2 Tianshu ${pkg.version}
82
+
83
+ tianshu init \u521D\u59CB\u5316\u72EC\u7ACB\u53F0\u8D26\uFF1B\u5DF2\u6709\u914D\u7F6E\u53EA\u6821\u9A8C\uFF0C\u4E0D\u8986\u76D6
84
+ tianshu doctor \u68C0\u67E5\u672C\u5305\u4F9D\u8D56\u3001\u914D\u7F6E\u548C\u53EA\u8BFB MCP \u67E5\u8BE2
85
+
86
+ \u9009\u9879\uFF1A--config <\u7EDD\u5BF9\u8DEF\u5F84>\uFF0C--help\uFF0C--version
87
+ \u73AF\u5883\uFF1ATIANSHU_CONFIG\u3001TIANSHU_HOME\u3001TIANSHU_PYTHON
88
+ \u63D2\u4EF6\u5B89\u88C5\u548C\u5378\u8F7D\u8BF7\u4F7F\u7528 Codex \u539F\u751F\u547D\u4EE4\uFF1B\u672C\u547D\u4EE4\u4E0D\u7BA1\u7406\u63D2\u4EF6\u3002`;
89
+ function run(python, args) {
90
+ return execFileSync(python, args, { encoding: "utf8", timeout: 3e4, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" } });
91
+ }
92
+ function pythonRuntime() {
93
+ const node = Number(process.versions.node.split(".")[0]);
94
+ if (node < 24 || node > 26) throw new Error("Node.js 24\u201326 is required");
95
+ const executable = process.env.TIANSHU_PYTHON || "python3";
96
+ const info = JSON.parse(run(executable, ["-c", 'import sys,json;print(json.dumps({"version":list(sys.version_info[:2]),"path":sys.executable}))']));
97
+ if (info.version[0] !== 3 || info.version[1] < 12) throw new Error("Python 3.12+ is required");
98
+ return info.path;
99
+ }
100
+ function validate(python, config) {
101
+ const result = JSON.parse(run(python, [join2(packageRoot, "ledger/read_ledger.py"), "--config", config]));
102
+ if (!result.sources?.length || result.sources.some((s) => !s.ok)) throw new Error("Configured ledger is missing or unhealthy; no replacement database was created");
103
+ }
104
+ async function main(argv = process.argv.slice(2)) {
105
+ if (!argv.length || argv.includes("--help")) return console.log(help);
106
+ if (argv.length === 1 && argv[0] === "--version") return console.log(pkg.version);
107
+ const [command, ...args] = argv;
108
+ if (!["init", "doctor"].includes(command)) throw new Error("Unsupported command. Use Codex native plugin commands for installation and removal.");
109
+ let explicit;
110
+ for (let i = 0; i < args.length; i++) {
111
+ if (args[i] === "--config" && args[i + 1] && !explicit) explicit = args[++i];
112
+ else throw new Error("Unknown option: " + args[i]);
113
+ }
114
+ const python = pythonRuntime(), config = configPath(explicit);
115
+ if (command === "doctor") {
116
+ validate(python, config);
117
+ const health = await probe(join2(packageRoot, "dist/server.mjs"), python, config);
118
+ return print({ ok: true, config, health, scope: "this_package", host_ui_verified: false });
119
+ }
120
+ if (existsSync(config)) {
121
+ validate(python, config);
122
+ return print({ initialized: false, config, data_preserved: true });
123
+ }
124
+ if (explicit || process.env.TIANSHU_CONFIG) throw new Error("Explicit config does not exist; init never creates a replacement for a specified configuration");
125
+ const directory = dirname(config);
126
+ if (existsSync(directory) && lstatSync(directory).isSymbolicLink()) throw new Error("Data home must not be a symbolic link");
127
+ mkdirSync(directory, { recursive: true, mode: 448 });
128
+ const data = join2(directory, "data");
129
+ if (existsSync(data) && lstatSync(data).isSymbolicLink()) throw new Error("Data directory must not be a symbolic link");
130
+ mkdirSync(data, { recursive: true, mode: 448 });
131
+ const db = join2(data, "coordinator.db");
132
+ if (!existsSync(db)) run(python, [join2(packageRoot, "ledger/coordinator.py"), "--db", db, "init"]);
133
+ const value = { sources: [{ id: "daily", label: "\u65E5\u5E38\u4EFB\u52A1", path: db }] };
134
+ run(python, ["-c", 'import sys;sys.path.insert(0,sys.argv[1]);from read_ledger import read_source;read_source({"id":"daily","label":"\u65E5\u5E38\u4EFB\u52A1","path":sys.argv[2]})', join2(packageRoot, "ledger"), db]);
135
+ writeFileSync(config, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 384 });
136
+ return print({ initialized: true, config, data_preserved: true });
137
+ }
138
+ export {
139
+ main
140
+ };