@kuandotdev/indicator 0.1.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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/package.json +37 -0
  3. package/src/cli.js +371 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @kuandotdev/indicator
2
+
3
+ Node wrapper CLI for Indicator.
4
+
5
+ This package is a production-shaped bootstrapper: it does not replace the Python
6
+ runtime. It wraps the existing repo commands for install, uninstall, diagnostics,
7
+ MCP/agent import, market data, and model forecast checks.
8
+
9
+ When no local Indicator repo is found, `install` clones
10
+ `https://github.com/cr2s367067/indicator.git` into `~/.indicator` before running
11
+ the installer. Override with `TV_INDICATOR_INSTALL_DIR` or
12
+ `TV_INDICATOR_REPO_URL`.
13
+
14
+ ## Local Development
15
+
16
+ Use the repo Makefile for local development installs and reinstalls:
17
+
18
+ ```bash
19
+ make install
20
+ make uninstall
21
+ make install FORCE_INSTALL=1
22
+ make agent-import PLATFORM=auto
23
+ make check-skills
24
+ ```
25
+
26
+ Use the wrapper directly only when testing the package itself:
27
+
28
+ ```bash
29
+ node src/cli.js --help
30
+ node src/cli.js doctor
31
+ ```
32
+
33
+ ## Production Install
34
+
35
+ Publish the package and expose:
36
+
37
+ ```bash
38
+ npx @kuandotdev/indicator install
39
+ npx @kuandotdev/indicator install --force
40
+ npx @kuandotdev/indicator uninstall --platform auto
41
+ ```
42
+
43
+ For IP protection, keep proprietary prompts/model orchestration server-side and
44
+ ship this package as a thin local installer/client.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@kuandotdev/indicator",
3
+ "version": "0.1.0",
4
+ "description": "Indicator CLI wrapper for install, uninstall, diagnostics, market data, and model forecast commands.",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "type": "module",
8
+ "bin": {
9
+ "indicator": "src/cli.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node src/cli.js",
13
+ "check": "node --check src/cli.js",
14
+ "test-install-wrapper": "node src/cli.js install --skip-wizard",
15
+ "test-uninstall-wrapper": "node src/cli.js uninstall --dry-run",
16
+ "doctor": "node src/cli.js doctor"
17
+ },
18
+ "keywords": [
19
+ "tradingview",
20
+ "indicator",
21
+ "mcp",
22
+ "agent",
23
+ "cli",
24
+ "installer"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "src",
34
+ "README.md",
35
+ "package.json"
36
+ ]
37
+ }
package/src/cli.js ADDED
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const TOOL_NAME = "indicator";
9
+ const DEFAULT_PLATFORM = "auto";
10
+ const DEFAULT_REPO_URL = "https://github.com/cr2s367067/indicator.git";
11
+
12
+ function printHelp() {
13
+ console.log(`Indicator CLI
14
+
15
+ Usage:
16
+ ${TOOL_NAME} <command> [options]
17
+
18
+ Commands:
19
+ install Run Indicator installer
20
+ uninstall Remove agent integrations or the full local install
21
+ doctor Run make doctor
22
+ status Show Indicator status
23
+ version Show Indicator version
24
+ check-skills Verify generated agent skills/prompts
25
+ session-check Verify TradingView browser session
26
+ search QUERY Search TradingView symbols
27
+ model-status Show embedded stock/ETF/index model status
28
+ model-forecast SYMBOL Read/refresh a model forecast row
29
+ ohlcv SYMBOL Fetch TradingView OHLCV data
30
+ rating SYMBOL Fetch raw TradingView technical rating
31
+ agent-import Import MCP/skills for agent platforms
32
+ agent-uninstall Remove MCP registrations for agent platforms
33
+ cli ...args Run the underlying training-view CLI through conda
34
+
35
+ Install options:
36
+ --force Set FORCE_INSTALL=1
37
+ --skip-wizard Set TV_INDICATOR_SETUP=0
38
+ --platform <name> auto | all | pi | claude-code | codex | gemini | cursor
39
+ --language <code> en | zhtw | zhcn | ...
40
+ --prompt-env Enable optional .env prompts during install
41
+ --plain-menu Use plain install menu
42
+
43
+ Uninstall options:
44
+ --dry-run Show what would be removed
45
+ --platform <name> Agent platform(s) for agent uninstall
46
+ --full Run make uninstall-all
47
+ --keep-env Preserve .env with --full
48
+
49
+ Examples:
50
+ ${TOOL_NAME} install --platform all --plain-menu
51
+ ${TOOL_NAME} uninstall --dry-run --platform all
52
+ ${TOOL_NAME} search 6584 --limit 5
53
+ ${TOOL_NAME} model-forecast NASDAQ:TSLA --json
54
+ ${TOOL_NAME} model-forecast BINANCE:BTCUSDT --refresh --json
55
+ ${TOOL_NAME} ohlcv BINANCE:BTCUSDT --interval 1D --bars 3
56
+ ${TOOL_NAME} cli data ohlcv TPEX:6584 --interval 1W --bars 40
57
+
58
+ Environment:
59
+ TV_INDICATOR_INSTALL_DIR Install/clone directory when no local repo is found
60
+ TV_INDICATOR_REPO_URL Git repo URL used for first-time bootstrap
61
+ TV_INDICATOR_PROJECT_ROOT Existing repo path to use instead of auto-detecting
62
+ `);
63
+ }
64
+
65
+ function parseOption(args, name) {
66
+ const index = args.indexOf(name);
67
+ if (index === -1) return undefined;
68
+ const value = args[index + 1];
69
+ if (!value || value.startsWith("--")) {
70
+ throw new Error(`${name} requires a value`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function hasFlag(args, name) {
76
+ return args.includes(name);
77
+ }
78
+
79
+ function removeOptions(args, optionsWithValues, flags) {
80
+ const out = [];
81
+ for (let i = 0; i < args.length; i += 1) {
82
+ const arg = args[i];
83
+ if (flags.includes(arg)) continue;
84
+ if (optionsWithValues.includes(arg)) {
85
+ i += 1;
86
+ continue;
87
+ }
88
+ out.push(arg);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ async function findProjectRoot({ bootstrap = false } = {}) {
94
+ const explicit = process.env.TV_INDICATOR_PROJECT_ROOT;
95
+ if (explicit) {
96
+ if (isProjectRoot(explicit)) {
97
+ return resolve(explicit);
98
+ }
99
+ throw new Error(`TV_INDICATOR_PROJECT_ROOT is not an Indicator project root: ${explicit}`);
100
+ }
101
+
102
+ const installDir = defaultInstallDir();
103
+ const starts = [process.cwd(), dirname(fileURLToPath(import.meta.url)), installDir];
104
+ for (const start of starts) {
105
+ let current = resolve(start);
106
+ while (current !== dirname(current)) {
107
+ if (isProjectRoot(current)) {
108
+ if (bootstrap && current === installDir) {
109
+ await updateProjectRoot(current);
110
+ }
111
+ return current;
112
+ }
113
+ current = dirname(current);
114
+ }
115
+ }
116
+
117
+ if (bootstrap) {
118
+ return await bootstrapProjectRoot(installDir);
119
+ }
120
+
121
+ throw new Error(
122
+ `Could not find Indicator project root. Run '${TOOL_NAME} install' first or set TV_INDICATOR_PROJECT_ROOT.`,
123
+ );
124
+ }
125
+
126
+ function defaultInstallDir() {
127
+ return resolve(process.env.TV_INDICATOR_INSTALL_DIR || resolve(homedir(), ".indicator"));
128
+ }
129
+
130
+ function isProjectRoot(path) {
131
+ return (
132
+ existsSync(resolve(path, "Makefile")) &&
133
+ existsSync(resolve(path, "pyproject.toml")) &&
134
+ existsSync(resolve(path, "tools", "interactive_install.sh"))
135
+ );
136
+ }
137
+
138
+ async function bootstrapProjectRoot(targetDir) {
139
+ if (existsSync(targetDir)) {
140
+ if (!isProjectRoot(targetDir)) {
141
+ throw new Error(
142
+ `Install directory exists but is not an Indicator project root: ${targetDir}`,
143
+ );
144
+ }
145
+ await updateProjectRoot(targetDir);
146
+ return targetDir;
147
+ }
148
+
149
+ const repoUrl = process.env.TV_INDICATOR_REPO_URL || DEFAULT_REPO_URL;
150
+ mkdirSync(dirname(targetDir), { recursive: true });
151
+ console.log(`Indicator repo not found; cloning ${repoUrl} into ${targetDir}`);
152
+ const cloneCode = await run({ command: ["git", "clone", repoUrl, targetDir] }, process.cwd());
153
+ if (cloneCode !== 0) {
154
+ throw new Error(`Failed to clone Indicator repo into ${targetDir}`);
155
+ }
156
+ return targetDir;
157
+ }
158
+
159
+ async function updateProjectRoot(root) {
160
+ if (!existsSync(resolve(root, ".git"))) return;
161
+ console.log(`Updating Indicator repo in ${root}`);
162
+ const code = await run({ command: ["git", "-C", root, "pull", "--ff-only"] }, process.cwd());
163
+ if (code !== 0) {
164
+ console.warn("Could not fast-forward update the Indicator repo; continuing with the existing checkout.");
165
+ }
166
+ }
167
+
168
+ async function run(spec, cwd) {
169
+ const env = { ...process.env, ...(spec.env ?? {}) };
170
+ console.log(`$ ${spec.command.join(" ")}`);
171
+ return await new Promise((resolveCode) => {
172
+ const proc = spawn(spec.command[0], spec.command.slice(1), {
173
+ cwd,
174
+ env,
175
+ stdio: "inherit",
176
+ });
177
+ proc.on("error", (error) => {
178
+ console.error(error.message);
179
+ resolveCode(1);
180
+ });
181
+ proc.on("close", (code) => {
182
+ resolveCode(code ?? 1);
183
+ });
184
+ });
185
+ }
186
+
187
+ function makeEnv(args) {
188
+ const env = {};
189
+ const platform = parseOption(args, "--platform");
190
+ const language = parseOption(args, "--language");
191
+ if (platform) env.PLATFORM = platform;
192
+ if (language) env.TV_AGENT_LANGUAGE = language;
193
+ if (hasFlag(args, "--force")) env.FORCE_INSTALL = "1";
194
+ if (hasFlag(args, "--skip-wizard")) env.TV_INDICATOR_SETUP = "0";
195
+ if (hasFlag(args, "--prompt-env")) env.TV_INSTALL_PROMPT_ENV = "1";
196
+ if (hasFlag(args, "--plain-menu")) env.TV_INSTALL_MENU = "plain";
197
+ return env;
198
+ }
199
+
200
+ function installCommand(args) {
201
+ return {
202
+ command: ["make", "install"],
203
+ env: makeEnv(args),
204
+ };
205
+ }
206
+
207
+ function uninstallCommand(args) {
208
+ const platform = parseOption(args, "--platform") ?? DEFAULT_PLATFORM;
209
+ if (hasFlag(args, "--full")) {
210
+ const env = {};
211
+ if (hasFlag(args, "--keep-env")) env.KEEP_ENV = "1";
212
+ return {
213
+ command: ["make", "uninstall-all"],
214
+ env,
215
+ };
216
+ }
217
+
218
+ return {
219
+ command: [
220
+ "./tools/uninstall_agent_tools.sh",
221
+ "--platform",
222
+ platform,
223
+ ...(hasFlag(args, "--dry-run") ? ["--dry-run"] : []),
224
+ ],
225
+ };
226
+ }
227
+
228
+ function makeTarget(target, extraEnv = {}) {
229
+ return {
230
+ command: ["make", target],
231
+ env: extraEnv,
232
+ };
233
+ }
234
+
235
+ function cliPassthroughCommand(args) {
236
+ if (args.length === 0) throw new Error("cli requires arguments for training-view");
237
+ const condaEnv = parseOption(args, "--conda-env") ?? "tv-indicator";
238
+ const cleaned = removeOptions(args, ["--conda-env"], []);
239
+ return {
240
+ command: [
241
+ "conda",
242
+ "run",
243
+ "-n",
244
+ condaEnv,
245
+ "--live-stream",
246
+ "--no-capture-output",
247
+ "python",
248
+ "-m",
249
+ "tv_indicator.cli.main",
250
+ ...cleaned,
251
+ ],
252
+ };
253
+ }
254
+
255
+ function agentImportCommand(args) {
256
+ return {
257
+ command: ["make", "agent-import"],
258
+ env: { PLATFORM: parseOption(args, "--platform") ?? DEFAULT_PLATFORM },
259
+ };
260
+ }
261
+
262
+ function agentUninstallCommand(args) {
263
+ return {
264
+ command: ["make", "agent-uninstall"],
265
+ env: { PLATFORM: parseOption(args, "--platform") ?? DEFAULT_PLATFORM },
266
+ };
267
+ }
268
+
269
+ function modelForecastCommand(args) {
270
+ const cleaned = removeOptions(args, ["--period"], ["--refresh", "--json"]);
271
+ const symbol = cleaned[0];
272
+ if (!symbol) throw new Error("model-forecast requires SYMBOL");
273
+ const env = { SYMBOL: symbol };
274
+ if (hasFlag(args, "--refresh")) env.REFRESH = "1";
275
+ if (hasFlag(args, "--json")) env.MODEL_JSON = "1";
276
+ const period = parseOption(args, "--period");
277
+ if (period) env.MODEL_PERIOD = period;
278
+ return makeTarget("model-forecast", env);
279
+ }
280
+
281
+ function searchCommand(args) {
282
+ const cleaned = removeOptions(args, ["--limit"], []);
283
+ const query = cleaned[0];
284
+ if (!query) throw new Error("search requires QUERY");
285
+ const env = { Q: query };
286
+ const limit = parseOption(args, "--limit");
287
+ if (limit) env.LIMIT = limit;
288
+ return makeTarget("search", env);
289
+ }
290
+
291
+ function ohlcvCommand(args) {
292
+ const cleaned = removeOptions(args, ["--interval", "--bars"], []);
293
+ const symbol = cleaned[0];
294
+ if (!symbol) throw new Error("ohlcv requires SYMBOL");
295
+ const env = { SYMBOL: symbol };
296
+ const interval = parseOption(args, "--interval");
297
+ const bars = parseOption(args, "--bars");
298
+ if (interval) env.INTERVAL = interval;
299
+ if (bars) env.BARS = bars;
300
+ return makeTarget("ohlcv", env);
301
+ }
302
+
303
+ function ratingCommand(args) {
304
+ const cleaned = removeOptions(args, ["--interval"], []);
305
+ const symbol = cleaned[0];
306
+ if (!symbol) throw new Error("rating requires SYMBOL");
307
+ const env = { SYMBOL: symbol };
308
+ const interval = parseOption(args, "--interval");
309
+ if (interval) env.INTERVAL = interval;
310
+ return makeTarget("rating", env);
311
+ }
312
+
313
+ function commandFor(args) {
314
+ const [command, ...rest] = args;
315
+ switch (command) {
316
+ case undefined:
317
+ case "-h":
318
+ case "--help":
319
+ case "help":
320
+ printHelp();
321
+ return null;
322
+ case "install":
323
+ return installCommand(rest);
324
+ case "uninstall":
325
+ return uninstallCommand(rest);
326
+ case "doctor":
327
+ return makeTarget("doctor");
328
+ case "status":
329
+ return makeTarget("status");
330
+ case "version":
331
+ return makeTarget("version");
332
+ case "check-skills":
333
+ return makeTarget("check-skills");
334
+ case "session-check":
335
+ return makeTarget("session-check");
336
+ case "search":
337
+ return searchCommand(rest);
338
+ case "model-status":
339
+ return makeTarget("model-status");
340
+ case "model-forecast":
341
+ return modelForecastCommand(rest);
342
+ case "ohlcv":
343
+ return ohlcvCommand(rest);
344
+ case "rating":
345
+ return ratingCommand(rest);
346
+ case "agent-import":
347
+ return agentImportCommand(rest);
348
+ case "agent-uninstall":
349
+ return agentUninstallCommand(rest);
350
+ case "cli":
351
+ return cliPassthroughCommand(rest);
352
+ default:
353
+ throw new Error(`Unknown command: ${command}`);
354
+ }
355
+ }
356
+
357
+ async function main() {
358
+ try {
359
+ const args = process.argv.slice(2);
360
+ const spec = commandFor(args);
361
+ if (!spec) return;
362
+ const root = await findProjectRoot({ bootstrap: args[0] === "install" });
363
+ const code = await run(spec, root);
364
+ process.exit(code);
365
+ } catch (error) {
366
+ console.error(error instanceof Error ? error.message : String(error));
367
+ process.exit(1);
368
+ }
369
+ }
370
+
371
+ await main();