@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-setup.js
CHANGED
|
@@ -1,121 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
settings.env
|
|
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
|
-
|
|
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 {
|
|
5
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
});
|