@intelli-1113/stratos-sdk 1.3.5 → 1.3.6

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/src/cli-doctor.js CHANGED
@@ -1,144 +1,127 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import { join } from "node:path";
6
-
7
- export async function doctorCommand() {
8
- console.log("[stratos-doctor] Running diagnostics...\n");
9
-
10
- const checks = [];
11
- let passed = 0;
12
- let failed = 0;
13
-
14
- const token = process.env.STRATOS_TOKEN;
15
- const url = process.env.STRATOS_URL || "https://stratos.anthropic.com";
16
-
17
- if (token && token.startsWith("sk_")) {
18
- console.log("✓ STRATOS_TOKEN set");
19
- passed++;
20
- } else {
21
- console.log("✗ STRATOS_TOKEN missing or invalid (must start with sk_)");
22
- checks.push("Set STRATOS_TOKEN environment variable with your enrollment token");
23
- failed++;
24
- }
25
-
26
- if (url && url.startsWith("https://")) {
27
- console.log(`✓ STRATOS_URL configured: ${url}`);
28
- passed++;
29
- } else {
30
- console.log("✗ STRATOS_URL missing or invalid (must be https://)");
31
- checks.push("Set STRATOS_URL environment variable");
32
- failed++;
33
- }
34
-
35
- if (process.env.NODE_OPTIONS?.includes("stratos-sdk/register")) {
36
- console.log("✓ NODE_OPTIONS includes stratos-sdk/register");
37
- passed++;
38
- } else {
39
- console.log("✗ NODE_OPTIONS not set to include stratos-sdk/register");
40
- checks.push(
41
- "Add to NODE_OPTIONS: --import @intelli-1113/stratos-sdk/register"
42
- );
43
- failed++;
44
- }
45
-
46
- if (process.env.STRATOS_RECORD_INPUTS === "0") {
47
- console.log("✓ STRATOS_RECORD_INPUTS=0 (content-off)");
48
- passed++;
49
- } else {
50
- console.log("⚠ STRATOS_RECORD_INPUTS not set to 0 (prompts may be recorded)");
51
- checks.push("Set STRATOS_RECORD_INPUTS=0 to disable prompt recording");
52
- }
53
-
54
- if (process.env.STRATOS_RECORD_OUTPUTS === "0") {
55
- console.log("✓ STRATOS_RECORD_OUTPUTS=0 (content-off)");
56
- passed++;
57
- } else {
58
- console.log(
59
- "⚠ STRATOS_RECORD_OUTPUTS not set to 0 (responses may be recorded)"
60
- );
61
- checks.push("Set STRATOS_RECORD_OUTPUTS=0 to disable response recording");
62
- }
63
-
64
- const claudeDir = join(homedir(), ".claude");
65
- const settingsPath = join(claudeDir, "settings.json");
66
- const envPath = join(claudeDir, ".env");
67
-
68
- if (existsSync(settingsPath)) {
69
- console.log(`✓ Settings file exists: ${settingsPath}`);
70
- passed++;
71
- } else {
72
- console.log(`✗ Settings file missing: ${settingsPath}`);
73
- checks.push("Run: stratos setup claude-code");
74
- failed++;
75
- }
76
-
77
- if (existsSync(envPath)) {
78
- console.log(`✓ Environment file exists: ${envPath}`);
79
- passed++;
80
- } else {
81
- console.log(`⚠ Env file missing: ${envPath}`);
82
- checks.push("Run: stratos setup claude-code");
83
- }
84
-
85
- if (token && url && failed === 0) {
86
- try {
87
- console.log("\n[stratos-doctor] Testing connectivity...");
88
- const response = await fetch(`${url}/api/health`, {
89
- method: "GET",
90
- headers: {
91
- "x-stratos-token": token,
92
- "user-agent": "stratos-sdk-doctor",
93
- },
94
- timeout: 5000,
95
- });
96
-
97
- if (response.ok) {
98
- console.log("✓ Connected to Stratos server");
99
- passed++;
100
- } else if (response.status === 401 || response.status === 403) {
101
- console.log(`✗ Stratos server rejected token (${response.status})`);
102
- checks.push("Verify STRATOS_TOKEN is correct");
103
- failed++;
104
- } else {
105
- console.log(`✗ Stratos server returned ${response.status}`);
106
- checks.push(`Check STRATOS_URL (got ${response.status})`);
107
- failed++;
108
- }
109
- } catch (err) {
110
- console.log(`✗ Could not connect to Stratos server: ${err.message}`);
111
- checks.push(
112
- `Check STRATOS_URL is reachable: ${url}\n` +
113
- `Common issues:\n` +
114
- ` - Network connectivity\n` +
115
- ` - Firewall blocking\n` +
116
- ` - Invalid URL`
117
- );
118
- failed++;
119
- }
120
- }
121
-
122
- console.log("\n" + "=".repeat(60));
123
- if (failed === 0) {
124
- console.log("✓ stratos-doctor: All checks passed");
125
- console.log(
126
- "\nYou are connected to Stratos. Your Claude usage will now be tracked."
127
- );
128
- } else {
129
- console.log(`✗ stratos-doctor: ${failed} check(s) failed\n`);
130
- if (checks.length > 0) {
131
- console.log("Remediation steps:");
132
- checks.forEach((check, i) => {
133
- console.log(` ${i + 1}. ${check}`);
134
- });
135
- }
136
- process.exit(1);
137
- }
138
- console.log("=".repeat(60) + "\n");
139
- }
140
-
141
- doctorCommand().catch((err) => {
142
- console.error("[stratos-doctor] Fatal error:", err.message);
143
- process.exit(1);
144
- });
1
+ // `stratos doctor` — check the Claude Code ↔ Stratos connection.
2
+ // Reads the files setup writes (not this shell's environment), so it gives the
3
+ // same answer no matter where it is run from.
4
+
5
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import spawn from "cross-spawn";
8
+ import {
9
+ HOOK_EVENTS,
10
+ claudeSettingsPath,
11
+ configExists,
12
+ configFile,
13
+ hasLegacyNodeOptions,
14
+ isStratosHook,
15
+ loadConfig,
16
+ stratosDir,
17
+ } from "./claude-code/settings.js";
18
+
19
+ function readJson(file) {
20
+ try {
21
+ return JSON.parse(readFileSync(file, "utf8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /** Paths quoted in a hook command, e.g. node "C:/…/cli.js" hook stop */
28
+ const quotedPaths = (cmd) => [...cmd.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
29
+
30
+ export async function doctorCommand() {
31
+ console.log("[stratos-doctor] Checking Claude Code → Stratos…\n");
32
+ let failed = 0;
33
+ const fixes = [];
34
+ const ok = (m) => console.log(`✓ ${m}`);
35
+ const warn = (m, fix) => {
36
+ console.log(`⚠ ${m}`);
37
+ if (fix) fixes.push(fix);
38
+ };
39
+ const fail = (m, fix) => {
40
+ console.log(`✗ ${m}`);
41
+ failed++;
42
+ if (fix) fixes.push(fix);
43
+ };
44
+
45
+ const cfg = loadConfig();
46
+ if (configExists()) ok(`Config: ${configFile()}`);
47
+ else if (cfg.token) warn("No config file — using STRATOS_* environment variables", "Run: stratos setup claude-code");
48
+ else fail("No Stratos config", "Run: stratos setup claude-code --token <token> --url <url>");
49
+
50
+ // The token format is the server's business; whether it is valid is checked below against Stratos itself.
51
+ if (cfg.token) ok("Token present");
52
+ else fail("Token missing", "Run: stratos setup claude-code --token <token>");
53
+
54
+ const local = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(cfg.url);
55
+ if (cfg.url.startsWith("https://") || local) ok(`Server: ${cfg.url}`);
56
+ else warn(`Server ${cfg.url} is plain http — token is sent unencrypted`);
57
+
58
+ const settingsPath = claudeSettingsPath();
59
+ const settings = existsSync(settingsPath) ? readJson(settingsPath) : null;
60
+ if (!settings) {
61
+ fail(`Cannot read ${settingsPath}`, "Run: stratos setup claude-code");
62
+ } else {
63
+ for (const event of Object.keys(HOOK_EVENTS)) {
64
+ const handlers = (settings.hooks?.[event] || []).flatMap((g) => g?.hooks || []);
65
+ const ours = handlers.find(isStratosHook);
66
+ if (!ours) {
67
+ fail(`Hook ${event} missing`, "Run: stratos setup claude-code");
68
+ continue;
69
+ }
70
+ const missing = quotedPaths(ours.command).filter((p) => !existsSync(p));
71
+ if (missing.length) fail(`Hook ${event} points at missing file: ${missing.join(", ")}`, "Re-run: stratos setup claude-code");
72
+ else ok(`Hook ${event}`);
73
+ }
74
+ const nodeOnPath = spawn.sync("node", ["--version"], { encoding: "utf8", timeout: 5000 });
75
+ if (nodeOnPath.status === 0) ok(`node on PATH (${nodeOnPath.stdout.trim()}) — hooks can start`);
76
+ else fail("`node` is not on PATH, so the hooks cannot start", "Add Node.js to PATH, then restart Claude Code");
77
+ if (hasLegacyNodeOptions(settings.env)) {
78
+ fail(
79
+ "settings.json env still sets NODE_OPTIONS=--import …stratos-sdk/register (breaks Node commands Claude runs)",
80
+ "Re-run: stratos setup claude-code (removes it)"
81
+ );
82
+ }
83
+ if (settings.env?.STRATOS_TOKEN) {
84
+ warn("settings.json env contains STRATOS_TOKEN — visible to every command Claude runs", "Re-run: stratos setup claude-code (moves it)");
85
+ }
86
+ }
87
+
88
+ const status = readJson(join(stratosDir(), "status.json")) || {};
89
+ if (status.lastSendAt) ok(`Last successful send: ${status.lastSendAt}`);
90
+ else console.log("· No usage sent yet (expected until a Claude Code session finishes a turn)");
91
+ if (status.lastError) warn(`Last error: ${status.lastError}`);
92
+
93
+ const outbox = join(stratosDir(), "outbox");
94
+ if (existsSync(outbox)) {
95
+ const files = readdirSync(outbox).filter((f) => f.endsWith(".json"));
96
+ if (files.length) {
97
+ const kb = Math.round(files.reduce((n, f) => n + statSync(join(outbox, f)).size, 0) / 1024);
98
+ warn(`${files.length} undelivered batch(es) waiting in outbox (${kb} KB) — sent on the next hook once Stratos is reachable`);
99
+ }
100
+ }
101
+
102
+ if (cfg.token) {
103
+ try {
104
+ const res = await fetch(`${cfg.url}/api/heartbeat`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json", "x-stratos-token": cfg.token },
107
+ body: JSON.stringify({ framework: "claude-code", model: null, tools: [], doctor: true }),
108
+ signal: AbortSignal.timeout(5000),
109
+ });
110
+ if (res.ok) ok("Stratos accepted the token");
111
+ else if (res.status === 401 || res.status === 403) fail(`Stratos rejected the token (${res.status})`, "Check the token in Stratos > Add agent");
112
+ else fail(`Stratos returned ${res.status}`, `Check the URL: ${cfg.url}`);
113
+ } catch (err) {
114
+ fail(`Cannot reach ${cfg.url}: ${err.cause?.code || err.message}`, "Check network, VPN or firewall, and the URL");
115
+ }
116
+ }
117
+
118
+ console.log("\n" + "=".repeat(60));
119
+ if (!failed) {
120
+ console.log("✓ All checks passed — new Claude Code sessions report usage to Stratos.");
121
+ } else {
122
+ console.log(`✗ ${failed} check(s) failed`);
123
+ [...new Set(fixes)].forEach((f, i) => console.log(` ${i + 1}. ${f}`));
124
+ process.exitCode = 1;
125
+ }
126
+ console.log("=".repeat(60));
127
+ }
package/src/cli-env.js ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { mkdir } from "node:fs/promises";
6
+
7
+ class EnvError extends Error {}
8
+
9
+ function isNodeProject() {
10
+ return existsSync(join(process.cwd(), "package.json"));
11
+ }
12
+
13
+ function envPath() {
14
+ return join(process.cwd(), ".env");
15
+ }
16
+
17
+ function readEnvFile(path) {
18
+ if (!existsSync(path)) return "";
19
+ try {
20
+ return readFileSync(path, "utf8");
21
+ } catch {
22
+ return "";
23
+ }
24
+ }
25
+
26
+ function backup(path) {
27
+ if (!existsSync(path)) return null;
28
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
29
+ const dest = `${path}.stratos-backup-${stamp}`;
30
+ try {
31
+ copyFileSync(path, dest);
32
+ return dest;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function parseEnvFile(content) {
39
+ const lines = content.split("\n");
40
+ const vars = new Map();
41
+ for (const line of lines) {
42
+ const trimmed = line.trim();
43
+ if (!trimmed || trimmed.startsWith("#")) continue;
44
+ const [key, ...valueParts] = trimmed.split("=");
45
+ if (key) vars.set(key, valueParts.join("="));
46
+ }
47
+ return vars;
48
+ }
49
+
50
+ function buildEnvContent(vars) {
51
+ return Array.from(vars.entries())
52
+ .map(([key, value]) => `${key}=${value}`)
53
+ .join("\n") + "\n";
54
+ }
55
+
56
+ export async function envAddCommand(...varStrings) {
57
+ try {
58
+ // Check if in a Node.js project
59
+ if (!isNodeProject()) {
60
+ throw new EnvError(
61
+ "Not a Node.js project. Run this command from your project root (where package.json is)."
62
+ );
63
+ }
64
+
65
+ // Validate we have at least one var
66
+ if (varStrings.length === 0) {
67
+ throw new EnvError("Invalid format. Use: stratos env add KEY=value [KEY2=value2 ...]");
68
+ }
69
+
70
+ // Parse and validate all variables
71
+ const newVars = new Map();
72
+ for (const varString of varStrings) {
73
+ if (!varString || !varString.includes("=")) {
74
+ throw new EnvError(`Invalid format: ${varString}. Use: KEY=value`);
75
+ }
76
+
77
+ const [key, ...valueParts] = varString.split("=");
78
+ if (!key || !key.match(/^[A-Z_][A-Z0-9_]*$/i)) {
79
+ throw new EnvError(`Invalid variable name: ${key}`);
80
+ }
81
+
82
+ newVars.set(key, valueParts.join("="));
83
+ }
84
+
85
+ const path = envPath();
86
+
87
+ // Read existing .env if it exists
88
+ const existingContent = readEnvFile(path);
89
+ const vars = parseEnvFile(existingContent);
90
+
91
+ // Create directory if needed
92
+ const dir = process.cwd();
93
+ await mkdir(dir, { recursive: true });
94
+
95
+ // Backup existing .env if present
96
+ let backupPath = null;
97
+ if (existsSync(path)) {
98
+ backupPath = backup(path);
99
+ }
100
+
101
+ // Process each new variable
102
+ for (const [key, value] of newVars.entries()) {
103
+ if (vars.has(key)) {
104
+ const oldValue = vars.get(key);
105
+ if (oldValue === value) {
106
+ console.log(`✓ ${key}=${value} already set`);
107
+ } else {
108
+ console.log(`✓ Updated ${key} (was: ${oldValue})`);
109
+ vars.set(key, value);
110
+ }
111
+ } else {
112
+ console.log(`✓ Added ${key}=${value}`);
113
+ vars.set(key, value);
114
+ }
115
+ }
116
+
117
+ // Show backup message if one was created
118
+ if (backupPath) {
119
+ console.log(` backup: ${backupPath}`);
120
+ }
121
+
122
+ // Write new .env file
123
+ const newContent = buildEnvContent(vars);
124
+ writeFileSync(path, newContent, "utf8");
125
+ console.log(`✓ Updated .env`);
126
+ } catch (err) {
127
+ if (err instanceof EnvError) {
128
+ console.error(`[stratos] ${err.message}`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ throw err;
133
+ }
134
+ }