@intelli-1113/stratos-sdk 1.3.4 → 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/README.md +4 -0
- package/package.json +17 -5
- 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 -0
- package/src/cli-env.js +134 -0
- package/src/cli-setup.js +171 -0
- package/src/cli.js +115 -0
- package/src/config-templates.js +148 -0
- package/src/enforcement.js +182 -163
- package/src/execution-spans.js +166 -0
- package/src/index.js +24 -3
- package/src/inspect-langchain.js +22 -2
- package/src/mcp-proxy.js +21 -3
- package/src/otel-bridge-harness.js +115 -0
- package/src/otel-bridge.js +170 -0
- package/src/postinstall.js +32 -11
package/src/cli-setup.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Config bundles for rolling Stratos out to many developers (MDM / scripts).
|
|
2
|
+
//
|
|
3
|
+
// Claude Code reports through hooks (see src/claude-code/), so a bundle is:
|
|
4
|
+
// - the hook block for ~/.claude/settings.json or managed-settings.json
|
|
5
|
+
// - the per-developer ~/.stratos/config.json (token + URL)
|
|
6
|
+
// Tokens come from Stratos enrollment; they are never invented here.
|
|
7
|
+
|
|
8
|
+
import { platform } from "node:os";
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { buildClaudeCodeHooks, MANAGED_SETTINGS_PATHS } from "./claude-code/settings.js";
|
|
12
|
+
|
|
13
|
+
const SDK_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
14
|
+
const SURFACES = ["claude-code"];
|
|
15
|
+
|
|
16
|
+
export class ConfigTemplate {
|
|
17
|
+
constructor(opts = {}) {
|
|
18
|
+
if (!opts.personEmail) throw new Error("ConfigTemplate: personEmail is required");
|
|
19
|
+
if (!opts.token) throw new Error("ConfigTemplate: token is required (issue one from Stratos enrollment)");
|
|
20
|
+
this.personEmail = opts.personEmail;
|
|
21
|
+
this.surface = opts.surface || "claude-code";
|
|
22
|
+
if (!SURFACES.includes(this.surface)) throw new Error(`ConfigTemplate: unsupported surface '${this.surface}'`);
|
|
23
|
+
this.token = opts.token;
|
|
24
|
+
this.url = (opts.url || "http://localhost:4000").replace(/\/+$/, "");
|
|
25
|
+
this.osType = opts.osType || platform();
|
|
26
|
+
this.recordContent = opts.recordContent === true;
|
|
27
|
+
// Paths of node and the SDK on the *target* machines, if they differ from this one.
|
|
28
|
+
this.hookPaths = { node: opts.nodePath, cli: opts.cliPath };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
hooks() {
|
|
32
|
+
const o = {};
|
|
33
|
+
if (this.hookPaths.node) o.node = this.hookPaths.node;
|
|
34
|
+
if (this.hookPaths.cli) o.cli = this.hookPaths.cli;
|
|
35
|
+
return buildClaudeCodeHooks(o);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Contents of ~/.claude/settings.json additions (merge into existing hooks). */
|
|
39
|
+
toSettingsJson() {
|
|
40
|
+
return { hooks: this.hooks() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Contents for managed-settings.json, enforced for every user on the machine. */
|
|
44
|
+
toManagedSettings() {
|
|
45
|
+
return { hooks: this.hooks() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
managedSettingsPath() {
|
|
49
|
+
return MANAGED_SETTINGS_PATHS[this.osType] || MANAGED_SETTINGS_PATHS.linux;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Contents of ~/.stratos/config.json for this developer. */
|
|
53
|
+
toStratosConfig() {
|
|
54
|
+
return {
|
|
55
|
+
token: this.token,
|
|
56
|
+
url: this.url,
|
|
57
|
+
appName: `claude-code:${this.personEmail.split("@")[0]}`,
|
|
58
|
+
recordInputs: this.recordContent,
|
|
59
|
+
recordOutputs: this.recordContent,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toBundle() {
|
|
64
|
+
return {
|
|
65
|
+
version: "2.0",
|
|
66
|
+
person: this.personEmail,
|
|
67
|
+
surface: this.surface,
|
|
68
|
+
os: this.osType,
|
|
69
|
+
claude_settings: this.toSettingsJson(),
|
|
70
|
+
managed_settings: { path: this.managedSettingsPath(), content: this.toManagedSettings() },
|
|
71
|
+
stratos_config: { path: "~/.stratos/config.json", content: this.toStratosConfig() },
|
|
72
|
+
metadata: { created_at: new Date().toISOString(), sdk_version: SDK_VERSION },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
save(dirPath) {
|
|
77
|
+
const name = `stratos-config-${this.personEmail.replace(/@.*/, "")}-${this.surface}.json`;
|
|
78
|
+
const jsonPath = join(dirPath, name);
|
|
79
|
+
writeFileSync(jsonPath, JSON.stringify(this.toBundle(), null, 2), { encoding: "utf8", mode: 0o600 });
|
|
80
|
+
return { jsonPath };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class MDMBundleGenerator {
|
|
85
|
+
constructor(opts = {}) {
|
|
86
|
+
this.orgUrl = opts.orgUrl || "http://localhost:4000";
|
|
87
|
+
this.adminToken = opts.adminToken;
|
|
88
|
+
this.templates = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
addTemplate(template) {
|
|
92
|
+
this.templates.push(template);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {Record<string,string>} tokens surface → enrollment token for this person */
|
|
96
|
+
generateForPerson(personEmail, tokens, opts = {}) {
|
|
97
|
+
return Object.entries(tokens).map(
|
|
98
|
+
([surface, token]) => new ConfigTemplate({ ...opts, personEmail, surface, token, url: this.orgUrl })
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toManifest() {
|
|
103
|
+
return {
|
|
104
|
+
version: "2.0",
|
|
105
|
+
org_url: this.orgUrl,
|
|
106
|
+
bundles: this.templates.map((t) => t.toBundle()),
|
|
107
|
+
metadata: {
|
|
108
|
+
created_at: new Date().toISOString(),
|
|
109
|
+
sdk_version: SDK_VERSION,
|
|
110
|
+
total_bundles: this.templates.length,
|
|
111
|
+
total_people: new Set(this.templates.map((t) => t.personEmail)).size,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async postToEnrollment(enrollmentUrl) {
|
|
117
|
+
if (!this.adminToken) {
|
|
118
|
+
console.error("[mdm-bundle] Admin token required to post enrollment");
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(`${enrollmentUrl.replace(/\/+$/, "")}/api/mdm/bundle`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.adminToken}` },
|
|
125
|
+
body: JSON.stringify(this.toManifest()),
|
|
126
|
+
signal: AbortSignal.timeout(10000),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
console.error(`[mdm-bundle] Enrollment returned ${res.status}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
console.log("[mdm-bundle] Bundle posted to enrollment successfully");
|
|
133
|
+
return true;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[mdm-bundle] Failed to post: ${err.message}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {Record<string,string>} tokensByEmail email → claude-code enrollment token */
|
|
142
|
+
export function createDefaultMDMBundle(tokensByEmail, orgUrl) {
|
|
143
|
+
const generator = new MDMBundleGenerator({ orgUrl });
|
|
144
|
+
for (const [email, token] of Object.entries(tokensByEmail)) {
|
|
145
|
+
generator.generateForPerson(email, { "claude-code": token }).forEach((t) => generator.addTemplate(t));
|
|
146
|
+
}
|
|
147
|
+
return generator;
|
|
148
|
+
}
|