@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/package.json +9 -8
- package/src/README.md +583 -0
- package/src/USAGE.md +299 -0
- package/src/claude-code/hook.js +309 -0
- package/src/claude-code/settings.js +155 -0
- package/src/claude-code/transcript.js +259 -0
- package/src/cli-doctor.js +127 -144
- package/src/cli-env.js +134 -0
- package/src/cli-setup.js +171 -121
- package/src/cli.js +115 -70
- package/src/config-templates.js +148 -183
- package/src/enforcement.js +182 -184
- package/src/index.js +4 -0
- package/src/postinstall.js +32 -11
package/src/cli-doctor.js
CHANGED
|
@@ -1,144 +1,127 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|