@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-setup.js CHANGED
@@ -1,121 +1,171 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
- import { homedir, platform } from "node:os";
5
- import { join, resolve } from "node:path";
6
- import { execSync } from "node:child_process";
7
-
8
- export async function setupCommand(surface) {
9
- if (!surface) {
10
- console.error("[stratos-setup] Error: Surface required");
11
- console.error("Usage: stratos setup <surface>");
12
- console.error("Example: stratos setup claude-code");
13
- process.exit(1);
14
- }
15
-
16
- if (surface !== "claude-code") {
17
- console.error(`[stratos-setup] Error: Unsupported surface '${surface}'`);
18
- console.error("Currently supported: claude-code");
19
- process.exit(1);
20
- }
21
-
22
- console.log(`[stratos-setup] Configuring Stratos for ${surface}...\n`);
23
-
24
- const claudeCodePath = detectClaudeCode();
25
- if (!claudeCodePath) {
26
- console.error("[stratos-setup] Error: Claude Code not detected on this machine");
27
- console.error("Install Claude Code from https://claude.ai/code before running setup.");
28
- process.exit(1);
29
- }
30
-
31
- console.log(`✓ Found Claude Code at: ${claudeCodePath}\n`);
32
-
33
- const token = process.env.STRATOS_TOKEN;
34
- if (!token) {
35
- console.error("[stratos-setup] Error: STRATOS_TOKEN environment variable not set");
36
- console.error("Provide your enrollment token via: export STRATOS_TOKEN=sk_...");
37
- process.exit(1);
38
- }
39
-
40
- const url = process.env.STRATOS_URL || "https://stratos.anthropic.com";
41
- console.log(`✓ Using Stratos server: ${url}\n`);
42
-
43
- const claudeDir = join(homedir(), ".claude");
44
- mkdirSync(claudeDir, { recursive: true });
45
-
46
- const envPath = join(claudeDir, ".env");
47
- const envContent = `STRATOS_TOKEN=${token}
48
- STRATOS_URL=${url}
49
- STRATOS_RECORD_INPUTS=0
50
- STRATOS_RECORD_OUTPUTS=0
51
- NODE_OPTIONS=--import @intelli-1113/stratos-sdk/register
52
- `;
53
-
54
- writeFileSync(envPath, envContent, "utf8");
55
- console.log(`✓ Wrote config to: ${envPath}`);
56
-
57
- const settingsPath = join(claudeDir, "settings.json");
58
- let settings = {};
59
- if (existsSync(settingsPath)) {
60
- try {
61
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
62
- } catch {
63
- console.warn("[stratos-setup] Warning: Could not parse existing settings.json, will merge");
64
- }
65
- }
66
-
67
- settings.env = settings.env || {};
68
- settings.env.STRATOS_TOKEN = token;
69
- settings.env.STRATOS_URL = url;
70
- settings.env.STRATOS_RECORD_INPUTS = "0";
71
- settings.env.STRATOS_RECORD_OUTPUTS = "0";
72
- settings.env.NODE_OPTIONS = "--import @intelli-1113/stratos-sdk/register";
73
-
74
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
75
- console.log(`✓ Updated: ${settingsPath}\n`);
76
-
77
- console.log("[stratos-setup] Setup complete. Running doctor to verify...\n");
78
-
79
- const { doctorCommand } = await import("./cli-doctor.js");
80
- await doctorCommand();
81
- }
82
-
83
- function detectClaudeCode() {
84
- const os = platform();
85
- const home = homedir();
86
-
87
- if (os === "darwin") {
88
- const macPath = join(home, "Applications", "Claude.app");
89
- if (existsSync(macPath)) return macPath;
90
- }
91
-
92
- if (os === "win32") {
93
- const paths = [
94
- join(home, "AppData", "Local", "Programs", "claude"),
95
- "C:\\Program Files\\Claude",
96
- "C:\\Program Files (x86)\\Claude",
97
- ];
98
- for (const p of paths) {
99
- if (existsSync(p)) return p;
100
- }
101
- }
102
-
103
- if (os === "linux") {
104
- const paths = [
105
- join(home, ".local", "share", "applications", "claude"),
106
- join(home, ".config", "claude"),
107
- ];
108
- for (const p of paths) {
109
- if (existsSync(p)) return p;
110
- }
111
- }
112
-
113
- try {
114
- const result = execSync("which claude 2>/dev/null || where claude 2>nul", {
115
- encoding: "utf8",
116
- }).trim();
117
- if (result) return result;
118
- } catch {}
119
-
120
- return null;
121
- }
1
+ // `stratos setup claude-code` — connect Claude Code to Stratos.
2
+ //
3
+ // Claude Code is a compiled binary, so the Node SDK cannot be loaded into it
4
+ // (NODE_OPTIONS only leaks into every Node command Claude runs). Instead we add
5
+ // four short-lived hooks to ~/.claude/settings.json; they read the session
6
+ // transcript and post usage to Stratos. No background process, nothing to
7
+ // restart after a reboot. The token stays in ~/.stratos/config.json.
8
+
9
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, rmSync } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ import spawn from "cross-spawn";
12
+ import {
13
+ DEFAULT_URL,
14
+ MANAGED_SETTINGS_PATHS,
15
+ buildClaudeCodeHooks,
16
+ claudeSettingsPath,
17
+ cleanLegacyEnv,
18
+ configFile,
19
+ hasLegacyNodeOptions,
20
+ loadConfig,
21
+ mergeStratosHooks,
22
+ removeStratosHooks,
23
+ saveConfig,
24
+ LEGACY_ENV_KEYS,
25
+ } from "./claude-code/settings.js";
26
+
27
+ class SetupError extends Error {}
28
+
29
+ function detectClaudeCode() {
30
+ // cross-spawn resolves npm's claude.cmd shim on Windows without going through a shell.
31
+ const r = spawn.sync("claude", ["--version"], { encoding: "utf8", timeout: 10000 });
32
+ return r.status === 0 && r.stdout ? r.stdout.trim() : null;
33
+ }
34
+
35
+ /** Read settings.json; refuse to continue if it exists but is not valid JSON. */
36
+ export function readSettings(path = claudeSettingsPath()) {
37
+ if (!existsSync(path)) return {};
38
+ const raw = readFileSync(path, "utf8");
39
+ if (!raw.trim()) return {};
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not a JSON object");
43
+ return parsed;
44
+ } catch (err) {
45
+ throw new SetupError(
46
+ `${path} is not valid JSON (${err.message}). Fix or move it, then re-run setup. Nothing was changed.`
47
+ );
48
+ }
49
+ }
50
+
51
+ function backup(path) {
52
+ if (!existsSync(path)) return null;
53
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
54
+ const dest = `${path}.stratos-backup-${stamp}`;
55
+ copyFileSync(path, dest);
56
+ return dest;
57
+ }
58
+
59
+ function writeSettings(path, settings) {
60
+ mkdirSync(dirname(path), { recursive: true });
61
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
62
+ }
63
+
64
+ /** Pure: apply our changes to a settings object. Exported for tests. */
65
+ export function applySetup(settings) {
66
+ const next = { ...settings };
67
+ next.hooks = mergeStratosHooks(settings.hooks || {});
68
+ if (settings.env) {
69
+ next.env = cleanLegacyEnv(settings.env);
70
+ if (!Object.keys(next.env).length) delete next.env;
71
+ }
72
+ return next;
73
+ }
74
+
75
+ /** Pure: remove our changes from a settings object. Exported for tests. */
76
+ export function applyRemove(settings) {
77
+ const next = { ...settings };
78
+ if (settings.hooks) {
79
+ next.hooks = removeStratosHooks(settings.hooks);
80
+ if (!Object.keys(next.hooks).length) delete next.hooks;
81
+ }
82
+ if (settings.env) {
83
+ next.env = cleanLegacyEnv(settings.env);
84
+ if (!Object.keys(next.env).length) delete next.env;
85
+ }
86
+ return next;
87
+ }
88
+
89
+ function printManaged() {
90
+ const snippet = { hooks: buildClaudeCodeHooks() };
91
+ console.log("Add this to managed-settings.json (merge with any existing hooks):\n");
92
+ console.log(JSON.stringify(snippet, null, 2));
93
+ console.log("\nLocations:");
94
+ for (const [os, p] of Object.entries(MANAGED_SETTINGS_PATHS)) console.log(` ${os.padEnd(7)} ${p}`);
95
+ console.log(
96
+ "\nThe paths above point at this machine's node and SDK install; adjust them for the target machines." +
97
+ "\nEach developer still needs ~/.stratos/config.json (token + URL): run `stratos setup claude-code --config-only`."
98
+ );
99
+ }
100
+
101
+ export async function setupCommand(surface, opts = {}) {
102
+ try {
103
+ if (opts["print-managed"]) return printManaged();
104
+
105
+ if (surface !== "claude-code") {
106
+ throw new SetupError(
107
+ surface ? `Unsupported surface '${surface}'. Currently supported: claude-code` : "Usage: stratos setup claude-code [--url URL] [--token TOKEN]"
108
+ );
109
+ }
110
+
111
+ const settingsPath = claudeSettingsPath();
112
+
113
+ if (opts.remove) {
114
+ const settings = readSettings(settingsPath);
115
+ const saved = backup(settingsPath);
116
+ writeSettings(settingsPath, applyRemove(settings));
117
+ rmSync(configFile(), { force: true });
118
+ console.log(`✓ Removed Stratos hooks from ${settingsPath}`);
119
+ if (saved) console.log(` backup: ${saved}`);
120
+ console.log(`✓ Deleted ${configFile()}`);
121
+ return;
122
+ }
123
+
124
+ console.log("[stratos-setup] Configuring Stratos for Claude Code…\n");
125
+
126
+ const version = detectClaudeCode();
127
+ if (version) console.log(`✓ Claude Code ${version}`);
128
+ else console.warn("⚠ `claude` not found on PATH — continuing (the desktop app also reads ~/.claude/settings.json)");
129
+
130
+ const current = loadConfig();
131
+ const token = opts.token || current.token;
132
+ if (!token) throw new SetupError("No token. Pass --token <enrollment token> or set STRATOS_TOKEN.");
133
+ const url = (opts.url || current.url || DEFAULT_URL).replace(/\/+$/, "");
134
+ if (!/^https?:\/\//.test(url)) throw new SetupError(`Invalid --url '${url}'`);
135
+ if (url.startsWith("http://") && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(url)) {
136
+ console.warn(`⚠ ${url} is plain http — the token will travel unencrypted`);
137
+ }
138
+
139
+ const cfgPath = saveConfig({
140
+ token,
141
+ url,
142
+ appName: opts["app-name"] || current.appName || "claude-code",
143
+ recordInputs: !!opts["record-prompts"],
144
+ recordOutputs: !!opts["record-prompts"],
145
+ });
146
+ console.log(`✓ Saved token and URL to ${cfgPath} (not in settings.json)`);
147
+ console.log(`✓ Stratos server: ${url}`);
148
+ console.log(`✓ Prompt/response text: ${opts["record-prompts"] ? "RECORDED (--record-prompts)" : "not recorded (default)"}`);
149
+
150
+ if (opts["config-only"]) return;
151
+
152
+ const settings = readSettings(settingsPath);
153
+ const hadLegacy = hasLegacyNodeOptions(settings.env) || LEGACY_ENV_KEYS.some((k) => settings.env?.[k] != null);
154
+ const saved = backup(settingsPath);
155
+ writeSettings(settingsPath, applySetup(settings));
156
+ console.log(`✓ Added Stratos hooks (SessionStart, Stop, SubagentStop, SessionEnd) to ${settingsPath}`);
157
+ if (saved) console.log(` backup: ${saved}`);
158
+ if (hadLegacy) console.log("✓ Removed NODE_OPTIONS / STRATOS_* entries left by an older setup");
159
+ console.log("\nNew Claude Code sessions will report usage. Running doctor…\n");
160
+
161
+ const { doctorCommand } = await import("./cli-doctor.js");
162
+ await doctorCommand();
163
+ } catch (err) {
164
+ if (err instanceof SetupError) {
165
+ console.error(`[stratos-setup] ${err.message}`);
166
+ process.exitCode = 1;
167
+ return;
168
+ }
169
+ throw err;
170
+ }
171
+ }
package/src/cli.js CHANGED
@@ -1,70 +1,115 @@
1
- #!/usr/bin/env node
2
-
3
- import { parseArgs } from "node:util";
4
- import { setupCommand } from "./cli-setup.js";
5
- import { doctorCommand } from "./cli-doctor.js";
6
-
7
- const VERSION = "1.3.4";
8
-
9
- function printHelp() {
10
- console.log(`
11
- stratos CLI ${VERSION}
12
-
13
- Usage:
14
- stratos <command> [options]
15
-
16
- Commands:
17
- setup <surface> Configure Stratos on a surface (e.g., claude-code)
18
- doctor Validate Stratos setup and connectivity
19
- --help, -h Show this message
20
- --version, -v Show version
21
-
22
- Examples:
23
- stratos setup claude-code
24
- stratos doctor
25
- `);
26
- }
27
-
28
- async function main() {
29
- const { values, positional } = parseArgs({
30
- options: {
31
- help: { type: "boolean", short: "h" },
32
- version: { type: "boolean", short: "v" },
33
- },
34
- allowPositionals: true,
35
- });
36
-
37
- if (values.help) {
38
- printHelp();
39
- process.exit(0);
40
- }
41
-
42
- if (values.version) {
43
- console.log(`stratos ${VERSION}`);
44
- process.exit(0);
45
- }
46
-
47
- const [command, ...args] = positional;
48
-
49
- if (!command) {
50
- printHelp();
51
- process.exit(0);
52
- }
53
-
54
- try {
55
- if (command === "setup") {
56
- await setupCommand(...args);
57
- } else if (command === "doctor") {
58
- await doctorCommand();
59
- } else {
60
- console.error(`Unknown command: ${command}`);
61
- printHelp();
62
- process.exit(1);
63
- }
64
- } catch (err) {
65
- console.error(`[stratos] Error:`, err.message);
66
- process.exit(1);
67
- }
68
- }
69
-
70
- main();
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
7
+
8
+ function printHelp() {
9
+ console.log(`
10
+ stratos CLI ${VERSION}
11
+
12
+ Getting started:
13
+ 1. Install: npm install @intelli-1113/stratos-sdk
14
+
15
+ 2. Add to your code (app.js):
16
+ node --import @intelli-1113/stratos-sdk/register app.js
17
+
18
+ OR in your code:
19
+ import Stratos from '@intelli-1113/stratos-sdk';
20
+ Stratos.start({ token: 'sk_...', url: 'https://...' });
21
+
22
+ 3. Set up config:
23
+ stratos env add STRATOS_TOKEN=sk_your_token
24
+ stratos env add STRATOS_URL=https://stratos.example.com
25
+
26
+ 4. Verify connection:
27
+ stratos doctor
28
+
29
+ Usage:
30
+ stratos <command> [options]
31
+
32
+ Commands:
33
+ setup <agent> Connect agent to Stratos (adds configuration)
34
+ --token <token> Enrollment token (or STRATOS_TOKEN)
35
+ --url <url> Stratos server (or STRATOS_URL; default http://localhost:4000)
36
+ --record-prompts Also send prompt and response text (off by default)
37
+ --config-only Only write ~/.stratos/config.json
38
+ --remove Remove configuration and ~/.stratos/config.json
39
+ --print-managed Print a managed-settings.json snippet for admins
40
+ doctor Check setup and connectivity
41
+ env add <VAR>=<value> [VAR2=value2...] Add environment variable(s) to .env (project directory only)
42
+ hook <event> Used by Claude Code hooks (session-start | stop | subagent-stop | session-end)
43
+ --help, -h Show this message
44
+ --version, -v Show version
45
+
46
+ Examples:
47
+ stratos setup claude-code --token <your-token> --url https://stratos.example.com
48
+ stratos doctor
49
+ stratos env add STRATOS_RECORD_PROMPTS=1
50
+ stratos env add STRATOS_TOKEN=sk_... STRATOS_URL=https://... STRATOS_RECORD_PROMPTS=1
51
+ `);
52
+ }
53
+
54
+ async function main() {
55
+ const argv = process.argv.slice(2);
56
+
57
+ // Hook path first: quiet, fast, never fails Claude Code.
58
+ if (argv[0] === "hook") {
59
+ try {
60
+ const { runHook, readStdin } = await import("./claude-code/hook.js");
61
+ await runHook(argv[1], await readStdin());
62
+ } catch {
63
+ /* runHook logs its own errors; nothing may reach Claude Code */
64
+ }
65
+ // Let fetch sockets finish closing first: exiting mid-close can trip a libuv assertion on Windows.
66
+ await new Promise((r) => setTimeout(r, 20));
67
+ process.exit(0);
68
+ }
69
+
70
+ const { values, positionals } = parseArgs({
71
+ args: argv,
72
+ options: {
73
+ help: { type: "boolean", short: "h" },
74
+ version: { type: "boolean", short: "v" },
75
+ token: { type: "string" },
76
+ url: { type: "string" },
77
+ "app-name": { type: "string" },
78
+ "record-prompts": { type: "boolean" },
79
+ "config-only": { type: "boolean" },
80
+ remove: { type: "boolean" },
81
+ "print-managed": { type: "boolean" },
82
+ },
83
+ allowPositionals: true,
84
+ });
85
+
86
+ if (values.version) return console.log(`stratos ${VERSION}`);
87
+ const [command, ...args] = positionals;
88
+ if (values.help || !command) return printHelp();
89
+
90
+ if (command === "setup") {
91
+ const { setupCommand } = await import("./cli-setup.js");
92
+ await setupCommand(args[0], values);
93
+ } else if (command === "doctor") {
94
+ const { doctorCommand } = await import("./cli-doctor.js");
95
+ await doctorCommand();
96
+ } else if (command === "env") {
97
+ if (args[0] === "add") {
98
+ const { envAddCommand } = await import("./cli-env.js");
99
+ await envAddCommand(...args.slice(1));
100
+ } else {
101
+ console.error(`Unknown env subcommand: ${args[0]}`);
102
+ console.log("Usage: stratos env add <VAR>=<value> [VAR2=value2 ...]");
103
+ process.exitCode = 1;
104
+ }
105
+ } else {
106
+ console.error(`Unknown command: ${command}`);
107
+ printHelp();
108
+ process.exitCode = 1;
109
+ }
110
+ }
111
+
112
+ main().catch((err) => {
113
+ console.error("[stratos] Error:", err?.message || err);
114
+ process.exit(1);
115
+ });