@simplr-ai/connect 0.3.0-dev.0

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 ADDED
@@ -0,0 +1,22 @@
1
+ # Simplr Connect
2
+
3
+ Simplr Connect enrolls a developer workstation and reports a bounded security inventory of installed AI tools, skill names, configured MCP servers, developer-tool versions, current Git repository and operating-system security posture.
4
+
5
+ ```bash
6
+ npx -y @simplr-ai/connect@dev enroll --api-url <simplr-api-url> --code <one-time-code>
7
+ ```
8
+
9
+ Use `simplr-connect sync` for a one-off inventory refresh or `simplr-connect watch` for a lightweight heartbeat every minute and full inventory reconciliation every 15 minutes.
10
+
11
+ To make an AI process remotely controllable, start it through the companion:
12
+
13
+ ```bash
14
+ simplr-connect run -- codex
15
+ simplr-connect run -- claude
16
+ ```
17
+
18
+ Pause, resume and stop commands are accepted only while Simplr Connect is supervising the process tree. Independently started AI processes remain visible in inventory but are never reported as remotely controlled. A signed privileged service is required for device-wide enforcement of unmanaged processes.
19
+
20
+ Security posture checks cover disk encryption, firewall, automatic updates, screen lock, endpoint protection and Secure Boot where the operating system permits an unprivileged check. Simplr receives only pass, fail, unknown or not-applicable status with fixed evidence labels. It does not upload raw command output, source files, skill contents, prompt contents, environment variables, shell history, usernames, local file paths or developer credentials.
21
+
22
+ The workstation credential is stored in macOS Keychain, Linux Secret Service or a Windows DPAPI-protected file. Enrollment fails when the platform credential store is unavailable rather than falling back to plaintext.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,778 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { execFileSync, spawn, spawnSync } from "child_process";
5
+ import { constants, unlinkSync } from "fs";
6
+ import {
7
+ access,
8
+ chmod,
9
+ mkdir,
10
+ open,
11
+ readFile,
12
+ readdir,
13
+ unlink,
14
+ writeFile
15
+ } from "fs/promises";
16
+ import { arch, homedir, hostname, platform } from "os";
17
+ import { basename, join } from "path";
18
+ var APP_VERSION = "0.3.0-dev.0";
19
+ var managedProcesses = /* @__PURE__ */ new Map();
20
+ function stateDirectory() {
21
+ if (platform() === "darwin")
22
+ return join(homedir(), "Library", "Application Support", "Simplr Connect");
23
+ if (platform() === "win32")
24
+ return join(
25
+ process.env.APPDATA || join(homedir(), "AppData", "Roaming"),
26
+ "Simplr Connect"
27
+ );
28
+ return join(
29
+ process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
30
+ "simplr-connect"
31
+ );
32
+ }
33
+ function statePath() {
34
+ return join(stateDirectory(), "state.json");
35
+ }
36
+ function encryptedCredentialPath() {
37
+ return join(stateDirectory(), "credential.bin");
38
+ }
39
+ function supervisorLockPath() {
40
+ return join(stateDirectory(), "supervisor.lock");
41
+ }
42
+ function apiUrl(override) {
43
+ const value = override || process.env.SIMPLR_API_URL;
44
+ if (!value) throw new Error("Use --api-url or set SIMPLR_API_URL");
45
+ const parsed = new URL(value);
46
+ const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
47
+ if (parsed.protocol !== "https:" && !local) throw new Error("Simplr API URL must use HTTPS");
48
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error("Simplr API URL cannot contain credentials, query parameters or a fragment");
49
+ return value.replace(/\/$/, "");
50
+ }
51
+ function osName() {
52
+ if (platform() === "darwin") return "macos";
53
+ if (platform() === "win32") return "windows";
54
+ return "linux";
55
+ }
56
+ function commandResult(command, args, timeout = 3e3) {
57
+ const result = spawnSync(command, args, {
58
+ encoding: "utf8",
59
+ shell: false,
60
+ timeout,
61
+ windowsHide: true
62
+ });
63
+ return {
64
+ ok: result.status === 0,
65
+ output: `${result.stdout || result.stderr}`.trim()
66
+ };
67
+ }
68
+ function commandVersion(command, args = ["--version"]) {
69
+ const result = commandResult(command, args);
70
+ if (!result.ok) return void 0;
71
+ return result.output.trim().split("\n")[0]?.slice(0, 100);
72
+ }
73
+ function detectAgents() {
74
+ const candidates = [
75
+ ["codex", "Codex", "codex"],
76
+ ["claude_code", "Claude Code", "claude"],
77
+ ["cursor", "Cursor", "cursor"]
78
+ ];
79
+ return candidates.flatMap(([kind, name, command]) => {
80
+ const version = commandVersion(command);
81
+ return version ? [{ kind, name, version, enabled: true }] : [];
82
+ });
83
+ }
84
+ function detectDeveloperTools() {
85
+ const candidates = [
86
+ { key: "git", name: "Git", category: "source_control", command: "git" },
87
+ {
88
+ key: "github_cli",
89
+ name: "GitHub CLI",
90
+ category: "source_control",
91
+ command: "gh",
92
+ authentication: () => commandResult("gh", ["auth", "status"]).ok ? "authenticated" : "not_authenticated"
93
+ },
94
+ {
95
+ key: "vscode",
96
+ name: "Visual Studio Code",
97
+ category: "ide",
98
+ command: "code"
99
+ },
100
+ { key: "cursor", name: "Cursor", category: "ide", command: "cursor" },
101
+ {
102
+ key: "docker",
103
+ name: "Docker",
104
+ category: "containers",
105
+ command: "docker"
106
+ },
107
+ {
108
+ key: "orbstack",
109
+ name: "OrbStack",
110
+ category: "containers",
111
+ command: "orb"
112
+ },
113
+ { key: "mise", name: "mise", category: "runtime", command: "mise" },
114
+ { key: "asdf", name: "asdf", category: "runtime", command: "asdf" },
115
+ { key: "node", name: "Node.js", category: "runtime", command: "node" },
116
+ { key: "bun", name: "Bun", category: "runtime", command: "bun" },
117
+ { key: "python", name: "Python", category: "runtime", command: "python3" },
118
+ {
119
+ key: "go",
120
+ name: "Go",
121
+ category: "runtime",
122
+ command: "go",
123
+ args: ["version"]
124
+ },
125
+ {
126
+ key: "java",
127
+ name: "Java",
128
+ category: "runtime",
129
+ command: "java",
130
+ args: ["-version"]
131
+ },
132
+ {
133
+ key: "onepassword",
134
+ name: "1Password CLI",
135
+ category: "secrets",
136
+ command: "op"
137
+ },
138
+ {
139
+ key: "vault",
140
+ name: "HashiCorp Vault",
141
+ category: "secrets",
142
+ command: "vault"
143
+ },
144
+ {
145
+ key: "gcloud",
146
+ name: "Google Cloud CLI",
147
+ category: "cloud",
148
+ command: "gcloud",
149
+ args: ["version"]
150
+ },
151
+ { key: "aws", name: "AWS CLI", category: "cloud", command: "aws" },
152
+ { key: "azure", name: "Azure CLI", category: "cloud", command: "az" },
153
+ {
154
+ key: "cloudflare",
155
+ name: "Cloudflare Wrangler",
156
+ category: "cloud",
157
+ command: "wrangler"
158
+ },
159
+ { key: "vercel", name: "Vercel CLI", category: "cloud", command: "vercel" },
160
+ {
161
+ key: "sentry",
162
+ name: "Sentry CLI",
163
+ category: "observability",
164
+ command: "sentry-cli"
165
+ },
166
+ {
167
+ key: "datadog",
168
+ name: "Datadog CI",
169
+ category: "observability",
170
+ command: "datadog-ci"
171
+ },
172
+ {
173
+ key: "jira",
174
+ name: "Jira CLI",
175
+ category: "work_management",
176
+ command: "jira"
177
+ },
178
+ {
179
+ key: "slack",
180
+ name: "Slack CLI",
181
+ category: "communication",
182
+ command: "slack"
183
+ }
184
+ ];
185
+ return candidates.map((candidate) => {
186
+ const version = commandVersion(candidate.command, candidate.args);
187
+ const installed = Boolean(version);
188
+ return {
189
+ key: candidate.key,
190
+ name: candidate.name,
191
+ category: candidate.category,
192
+ version,
193
+ installed,
194
+ authentication: !installed ? "not_applicable" : candidate.authentication ? candidate.authentication() : "unknown"
195
+ };
196
+ });
197
+ }
198
+ async function pathExists(path) {
199
+ try {
200
+ await access(path, constants.F_OK);
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+ function postureCheck(key, status, evidence) {
207
+ return { key, status, evidence };
208
+ }
209
+ async function macosPosture() {
210
+ const encryption = commandResult("/usr/bin/fdesetup", ["status"]);
211
+ const firewall = commandResult(
212
+ "/usr/libexec/ApplicationFirewall/socketfilterfw",
213
+ ["--getglobalstate"]
214
+ );
215
+ const updates = commandResult("/usr/sbin/softwareupdate", ["--schedule"]);
216
+ const screenLock = commandResult("/usr/bin/defaults", [
217
+ "-currentHost",
218
+ "read",
219
+ "com.apple.screensaver",
220
+ "askForPassword"
221
+ ]);
222
+ const endpointProtection = await pathExists(
223
+ "/Library/Apple/System/Library/CoreServices/XProtect.bundle"
224
+ ) || await pathExists("/Applications/Falcon.app") || await pathExists("/Applications/SentinelOne");
225
+ return [
226
+ postureCheck(
227
+ "disk_encryption",
228
+ encryption.ok ? /FileVault is On/i.test(encryption.output) ? "pass" : "fail" : "unknown",
229
+ encryption.ok ? "FileVault state verified" : "FileVault state unavailable"
230
+ ),
231
+ postureCheck(
232
+ "firewall",
233
+ firewall.ok ? /enabled|state\s*=\s*1/i.test(firewall.output) ? "pass" : "fail" : "unknown",
234
+ firewall.ok ? "Application firewall state verified" : "Firewall state unavailable"
235
+ ),
236
+ postureCheck(
237
+ "automatic_updates",
238
+ updates.ok ? /on/i.test(updates.output) ? "pass" : "fail" : "unknown",
239
+ updates.ok ? "Automatic update schedule verified" : "Automatic update state unavailable"
240
+ ),
241
+ postureCheck(
242
+ "screen_lock",
243
+ screenLock.ok ? /^1$|true/i.test(screenLock.output) ? "pass" : "fail" : "unknown",
244
+ screenLock.ok ? "Screen-lock password requirement verified" : "Screen-lock state unavailable"
245
+ ),
246
+ postureCheck(
247
+ "endpoint_protection",
248
+ endpointProtection ? "pass" : "unknown",
249
+ endpointProtection ? "Built-in or managed endpoint protection detected" : "Endpoint protection could not be verified"
250
+ ),
251
+ postureCheck(
252
+ "secure_boot",
253
+ "unknown",
254
+ "Secure Boot requires elevated or MDM attestation"
255
+ )
256
+ ];
257
+ }
258
+ async function windowsPosture() {
259
+ const powershell = (script) => commandResult(
260
+ "powershell.exe",
261
+ ["-NoProfile", "-NonInteractive", "-Command", script],
262
+ 5e3
263
+ );
264
+ const encryption = commandResult("manage-bde.exe", ["-status", "C:"]);
265
+ const firewall = powershell(
266
+ "if ((Get-NetFirewallProfile | Where-Object Enabled).Count -eq 3) { 'pass' } else { 'fail' }"
267
+ );
268
+ const updates = powershell(
269
+ "$v=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\WindowsUpdate\\UX\\Settings' -ErrorAction SilentlyContinue).UxOption; if ($null -eq $v -or $v -ne 1) { 'pass' } else { 'fail' }"
270
+ );
271
+ const screenLock = powershell(
272
+ "$v=(Get-ItemProperty 'HKCU:\\Control Panel\\Desktop' -ErrorAction SilentlyContinue).ScreenSaveActive; if ($v -eq '1') { 'pass' } else { 'fail' }"
273
+ );
274
+ const endpoint = powershell(
275
+ "$v=Get-MpComputerStatus -ErrorAction SilentlyContinue; if ($v.AMServiceEnabled -and $v.RealTimeProtectionEnabled) { 'pass' } else { 'fail' }"
276
+ );
277
+ const secureBoot = powershell(
278
+ "try { if (Confirm-SecureBootUEFI) { 'pass' } else { 'fail' } } catch { 'unknown' }"
279
+ );
280
+ const state = (result) => result.ok && /pass/i.test(result.output) ? "pass" : result.ok && /fail/i.test(result.output) ? "fail" : "unknown";
281
+ return [
282
+ postureCheck(
283
+ "disk_encryption",
284
+ encryption.ok ? /Protection Status:\s+Protection On/i.test(encryption.output) ? "pass" : "fail" : "unknown",
285
+ "BitLocker protection state checked"
286
+ ),
287
+ postureCheck(
288
+ "firewall",
289
+ state(firewall),
290
+ "Windows Firewall profiles checked"
291
+ ),
292
+ postureCheck(
293
+ "automatic_updates",
294
+ state(updates),
295
+ "Windows Update policy checked"
296
+ ),
297
+ postureCheck(
298
+ "screen_lock",
299
+ state(screenLock),
300
+ "Screen-lock policy checked"
301
+ ),
302
+ postureCheck(
303
+ "endpoint_protection",
304
+ state(endpoint),
305
+ "Microsoft Defender real-time protection checked"
306
+ ),
307
+ postureCheck(
308
+ "secure_boot",
309
+ state(secureBoot),
310
+ "UEFI Secure Boot state checked"
311
+ )
312
+ ];
313
+ }
314
+ async function linuxPosture() {
315
+ const rootSource = commandResult("findmnt", ["-no", "SOURCE", "/"]);
316
+ const rootDevice = rootSource.ok && /^\/dev\/[A-Za-z0-9._/+:-]+$/.test(rootSource.output) ? rootSource.output : "";
317
+ const encryption = rootDevice ? commandResult("lsblk", ["-s", "-no", "TYPE", rootDevice]) : { ok: false, output: "" };
318
+ const ufw = commandResult("ufw", ["status"]);
319
+ const firewalld = commandResult("firewall-cmd", ["--state"]);
320
+ const unattended = commandResult("systemctl", [
321
+ "is-enabled",
322
+ "unattended-upgrades"
323
+ ]);
324
+ const dnfAutomatic = commandResult("systemctl", [
325
+ "is-enabled",
326
+ "dnf-automatic.timer"
327
+ ]);
328
+ const secureBoot = commandResult("mokutil", ["--sb-state"]);
329
+ const endpointProtection = await pathExists("/opt/CrowdStrike/falconctl") || await pathExists("/opt/sentinelone/bin/sentinelctl") || await pathExists("/opt/microsoft/mdatp/sbin/wdavdaemon");
330
+ const firewallEnabled = ufw.ok && /Status:\s+active/i.test(ufw.output) || firewalld.ok && /running/i.test(firewalld.output);
331
+ return [
332
+ postureCheck(
333
+ "disk_encryption",
334
+ encryption.ok ? /crypt/i.test(encryption.output) ? "pass" : "fail" : "unknown",
335
+ "Encrypted block-device state checked"
336
+ ),
337
+ postureCheck(
338
+ "firewall",
339
+ ufw.ok || firewalld.ok ? firewallEnabled ? "pass" : "fail" : "unknown",
340
+ "Host firewall state checked"
341
+ ),
342
+ postureCheck(
343
+ "automatic_updates",
344
+ unattended.ok || dnfAutomatic.ok ? "pass" : "unknown",
345
+ "Automatic security update service checked"
346
+ ),
347
+ postureCheck(
348
+ "screen_lock",
349
+ "unknown",
350
+ "Desktop screen-lock policy requires MDM attestation"
351
+ ),
352
+ postureCheck(
353
+ "endpoint_protection",
354
+ endpointProtection ? "pass" : "unknown",
355
+ endpointProtection ? "Managed endpoint protection detected" : "Endpoint protection could not be verified"
356
+ ),
357
+ postureCheck(
358
+ "secure_boot",
359
+ secureBoot.ok ? /enabled/i.test(secureBoot.output) ? "pass" : "fail" : "unknown",
360
+ "Secure Boot state checked"
361
+ )
362
+ ];
363
+ }
364
+ async function detectSecurityPosture() {
365
+ const checks = platform() === "darwin" ? await macosPosture() : platform() === "win32" ? await windowsPosture() : await linuxPosture();
366
+ return { collected_at: (/* @__PURE__ */ new Date()).toISOString(), checks };
367
+ }
368
+ async function existingDirectories(paths) {
369
+ const found = [];
370
+ for (const path of paths) {
371
+ try {
372
+ await access(path, constants.R_OK);
373
+ found.push(path);
374
+ } catch {
375
+ }
376
+ }
377
+ return found;
378
+ }
379
+ async function detectSkills() {
380
+ const paths = await existingDirectories([
381
+ join(homedir(), ".agents", "skills"),
382
+ join(homedir(), ".claude", "skills"),
383
+ join(process.cwd(), ".agents", "skills"),
384
+ join(process.cwd(), ".claude", "skills")
385
+ ]);
386
+ const skills = /* @__PURE__ */ new Map();
387
+ for (const path of paths) {
388
+ const entries = await readdir(path, { withFileTypes: true }).catch(
389
+ () => []
390
+ );
391
+ for (const entry of entries) {
392
+ if (!entry.isDirectory()) continue;
393
+ const source = path.startsWith(process.cwd()) ? "repository" : "local";
394
+ skills.set(`${source}:${entry.name}`, {
395
+ key: entry.name,
396
+ name: entry.name,
397
+ source,
398
+ enabled: true
399
+ });
400
+ }
401
+ }
402
+ return [...skills.values()].slice(0, 500);
403
+ }
404
+ async function detectMcpServers() {
405
+ const configPaths = await existingDirectories([
406
+ join(homedir(), ".codex", "config.toml"),
407
+ join(homedir(), ".claude.json"),
408
+ join(process.cwd(), ".mcp.json")
409
+ ]);
410
+ const names = /* @__PURE__ */ new Set();
411
+ for (const path of configPaths) {
412
+ const content = await readFile(path, "utf8").catch(() => "");
413
+ if (/simplr-dev|@simplr-ai\/dev-mcp/i.test(content))
414
+ names.add("simplr-dev");
415
+ if (/github/i.test(content)) names.add("github");
416
+ if (/sentry/i.test(content)) names.add("sentry");
417
+ if (/figma/i.test(content)) names.add("figma");
418
+ }
419
+ return [...names].map((name) => ({ name, status: "configured" }));
420
+ }
421
+ function detectRepository() {
422
+ try {
423
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
424
+ encoding: "utf8",
425
+ timeout: 3e3
426
+ }).trim();
427
+ const branch = execFileSync("git", ["branch", "--show-current"], {
428
+ encoding: "utf8",
429
+ timeout: 3e3,
430
+ cwd: root
431
+ }).trim();
432
+ return [{ name: basename(root), branch: branch || void 0 }];
433
+ } catch {
434
+ return [];
435
+ }
436
+ }
437
+ async function inventory() {
438
+ return {
439
+ agents: detectAgents(),
440
+ skills: await detectSkills(),
441
+ mcp_servers: await detectMcpServers(),
442
+ repositories: detectRepository(),
443
+ active_runs: [...managedProcesses.entries()].map(([pid, process2]) => ({
444
+ id: `${pid}`,
445
+ agent: process2.agent,
446
+ label: process2.label,
447
+ status: process2.paused ? "blocked" : "running",
448
+ started_at: process2.started_at
449
+ })),
450
+ developer_tools: detectDeveloperTools(),
451
+ security_posture: await detectSecurityPosture()
452
+ };
453
+ }
454
+ async function post(url, body, token) {
455
+ const response = await fetch(url, {
456
+ method: "POST",
457
+ headers: {
458
+ "Content-Type": "application/json",
459
+ ...token ? { Authorization: `Bearer ${token}` } : {}
460
+ },
461
+ body: JSON.stringify(body),
462
+ signal: AbortSignal.timeout(1e4)
463
+ });
464
+ const payload = await response.json().catch(() => ({}));
465
+ if (!response.ok || !payload.content)
466
+ throw new Error(
467
+ payload.message || `Simplr request failed (${response.status})`
468
+ );
469
+ return payload.content;
470
+ }
471
+ async function saveState(state) {
472
+ await mkdir(stateDirectory(), { recursive: true, mode: 448 });
473
+ const { device_token: legacyToken, ...safeState } = state;
474
+ void legacyToken;
475
+ await writeFile(statePath(), `${JSON.stringify(safeState, null, 2)}
476
+ `, {
477
+ encoding: "utf8",
478
+ mode: 384
479
+ });
480
+ if (platform() !== "win32") await chmod(statePath(), 384);
481
+ }
482
+ async function loadState() {
483
+ const state = JSON.parse(await readFile(statePath(), "utf8"));
484
+ state.api_url = apiUrl(state.api_url);
485
+ if (state.device_token) {
486
+ await storeCredential(state.workstation_id, state.device_token);
487
+ delete state.device_token;
488
+ await saveState(state);
489
+ }
490
+ return state;
491
+ }
492
+ function credentialService(workstationId) {
493
+ return `simplr-connect:${workstationId}`;
494
+ }
495
+ async function storeCredential(workstationId, token) {
496
+ await mkdir(stateDirectory(), { recursive: true, mode: 448 });
497
+ if (platform() === "darwin") {
498
+ const result2 = spawnSync("/usr/bin/security", ["add-generic-password", "-U", "-a", workstationId, "-s", "Simplr Connect", "-w"], {
499
+ input: `${token}
500
+ `,
501
+ encoding: "utf8",
502
+ timeout: 5e3
503
+ });
504
+ if (result2.status !== 0) throw new Error("Could not protect the workstation credential in macOS Keychain");
505
+ return;
506
+ }
507
+ if (platform() === "linux") {
508
+ const result2 = spawnSync("secret-tool", ["store", "--label=Simplr Connect", "service", "simplr-connect", "workstation", workstationId], {
509
+ input: `${token}
510
+ `,
511
+ encoding: "utf8",
512
+ timeout: 5e3
513
+ });
514
+ if (result2.status !== 0) throw new Error("Install and unlock Secret Service support before enrolling this workstation");
515
+ return;
516
+ }
517
+ const script = "$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)";
518
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { input: token, encoding: "utf8", timeout: 5e3, windowsHide: true });
519
+ if (result.status !== 0 || !result.stdout.trim()) throw new Error("Could not protect the workstation credential with Windows DPAPI");
520
+ await writeFile(encryptedCredentialPath(), result.stdout.trim(), { encoding: "utf8", mode: 384 });
521
+ }
522
+ async function loadCredential(state) {
523
+ if (state.device_token) return state.device_token;
524
+ if (platform() === "darwin") {
525
+ const result = commandResult("/usr/bin/security", ["find-generic-password", "-a", state.workstation_id, "-s", "Simplr Connect", "-w"], 5e3);
526
+ if (result.ok && result.output) return result.output;
527
+ } else if (platform() === "linux") {
528
+ const result = commandResult("secret-tool", ["lookup", "service", "simplr-connect", "workstation", state.workstation_id], 5e3);
529
+ if (result.ok && result.output) return result.output;
530
+ } else {
531
+ const encrypted = await readFile(encryptedCredentialPath(), "utf8");
532
+ const script = "$e=[Convert]::FromBase64String([Console]::In.ReadToEnd());$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)";
533
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { input: encrypted, encoding: "utf8", timeout: 5e3, windowsHide: true });
534
+ if (result.status === 0 && result.stdout.trim()) return result.stdout.trim();
535
+ }
536
+ throw new Error(`Protected credential ${credentialService(state.workstation_id)} is unavailable; re-enroll this workstation`);
537
+ }
538
+ async function enroll(code, apiUrlOverride) {
539
+ const enrollmentApiUrl = apiUrl(apiUrlOverride);
540
+ const content = await post(`${enrollmentApiUrl}/v1/ai-workstations/enroll`, {
541
+ code,
542
+ name: hostname(),
543
+ os: osName(),
544
+ architecture: arch(),
545
+ app_version: APP_VERSION
546
+ });
547
+ await storeCredential(content.workstation.id, content.device_token);
548
+ const state = {
549
+ api_url: enrollmentApiUrl,
550
+ workstation_id: content.workstation.id,
551
+ organization_name: content.organization.name,
552
+ control_state: "running"
553
+ };
554
+ await saveState(state);
555
+ await sync(state);
556
+ process.stdout.write(
557
+ `Connected ${hostname()} to ${state.organization_name}.
558
+ Run "simplr-connect watch" to keep inventory online.
559
+ `
560
+ );
561
+ }
562
+ async function sync(existingState) {
563
+ const state = existingState || await loadState();
564
+ const token = await loadCredential(state);
565
+ const response = await post(
566
+ `${state.api_url}/v1/ai-workstations/heartbeat`,
567
+ {
568
+ app_version: APP_VERSION,
569
+ control_state: state.control_state || "running",
570
+ accept_control_commands: managedProcesses.size > 0,
571
+ inventory: await inventory()
572
+ },
573
+ token
574
+ );
575
+ process.stdout.write(`Inventory synced at ${(/* @__PURE__ */ new Date()).toISOString()}.
576
+ `);
577
+ if (response.command) await applyCommand(state, response.command, true);
578
+ }
579
+ async function heartbeat(state) {
580
+ const token = await loadCredential(state);
581
+ const response = await post(
582
+ `${state.api_url}/v1/ai-workstations/heartbeat`,
583
+ {
584
+ app_version: APP_VERSION,
585
+ control_state: state.control_state || "running",
586
+ accept_control_commands: managedProcesses.size > 0
587
+ },
588
+ token
589
+ );
590
+ if (response.command) await applyCommand(state, response.command, false);
591
+ }
592
+ function signalManagedProcesses(signal) {
593
+ if (platform() === "win32") throw new Error("Pause and resume require the signed Windows service and are not available in companion mode");
594
+ for (const [pid, managed] of managedProcesses) {
595
+ process.kill(-pid, signal);
596
+ managed.paused = signal === "SIGSTOP";
597
+ }
598
+ }
599
+ async function stopManagedProcesses() {
600
+ if (managedProcesses.size === 0) return "No Simplr-managed AI processes were running";
601
+ const processes = [...managedProcesses.entries()];
602
+ for (const [pid] of processes) {
603
+ if (platform() === "win32") {
604
+ const result = commandResult("taskkill.exe", ["/PID", `${pid}`, "/T", "/F"], 1e4);
605
+ if (!result.ok) throw new Error(`Windows could not terminate managed process ${pid}`);
606
+ } else {
607
+ process.kill(-pid, "SIGTERM");
608
+ }
609
+ }
610
+ if (platform() !== "win32") {
611
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
612
+ for (const [pid] of processes) {
613
+ if (!managedProcesses.has(pid)) continue;
614
+ try {
615
+ process.kill(-pid, "SIGKILL");
616
+ } catch {
617
+ }
618
+ }
619
+ }
620
+ return `${processes.length} Simplr-managed AI process${processes.length === 1 ? "" : "es"} terminated`;
621
+ }
622
+ async function acquireSupervisorLock() {
623
+ await mkdir(stateDirectory(), { recursive: true, mode: 448 });
624
+ const lockPath = supervisorLockPath();
625
+ const create = async () => {
626
+ const handle = await open(lockPath, "wx", 384);
627
+ await handle.writeFile(`${process.pid}
628
+ `);
629
+ await handle.close();
630
+ };
631
+ try {
632
+ await create();
633
+ } catch (error) {
634
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
635
+ const existingPid = Number.parseInt((await readFile(lockPath, "utf8").catch(() => "0")).trim(), 10);
636
+ let active = false;
637
+ if (existingPid > 0) {
638
+ try {
639
+ process.kill(existingPid, 0);
640
+ active = true;
641
+ } catch {
642
+ }
643
+ }
644
+ if (active) throw new Error("Another Simplr-managed AI process is already active on this workstation");
645
+ await unlink(lockPath).catch(() => void 0);
646
+ await create();
647
+ }
648
+ const release = () => {
649
+ try {
650
+ unlinkSync(lockPath);
651
+ } catch {
652
+ }
653
+ };
654
+ process.once("exit", release);
655
+ return release;
656
+ }
657
+ async function applyCommand(state, command, inventoryAlreadySynced) {
658
+ let result = "Command applied";
659
+ let status = "completed";
660
+ try {
661
+ if (command.type === "pause_agents") {
662
+ signalManagedProcesses("SIGSTOP");
663
+ state.control_state = "paused";
664
+ result = `${managedProcesses.size} Simplr-managed process${managedProcesses.size === 1 ? "" : "es"} paused`;
665
+ }
666
+ if (command.type === "resume_agents") {
667
+ signalManagedProcesses("SIGCONT");
668
+ state.control_state = "running";
669
+ result = `${managedProcesses.size} Simplr-managed process${managedProcesses.size === 1 ? "" : "es"} resumed`;
670
+ }
671
+ if (command.type === "stop_all") {
672
+ result = await stopManagedProcesses();
673
+ state.control_state = "stopped";
674
+ }
675
+ if (command.type === "sync_inventory" && !inventoryAlreadySynced) await sync(state);
676
+ await saveState(state);
677
+ } catch (error) {
678
+ status = "failed";
679
+ result = error instanceof Error ? error.message : "Command could not be applied";
680
+ }
681
+ const token = await loadCredential(state);
682
+ await post(`${state.api_url}/v1/ai-workstations/commands/${command.id}/acknowledge`, { status, result }, token);
683
+ }
684
+ async function watch() {
685
+ const state = await loadState();
686
+ await sync(state);
687
+ setInterval(
688
+ () => void heartbeat(state).catch(
689
+ (error) => process.stderr.write(
690
+ `${error instanceof Error ? error.message : "Heartbeat failed"}
691
+ `
692
+ )
693
+ ),
694
+ 6e4
695
+ );
696
+ setInterval(
697
+ () => void sync(state).catch(
698
+ (error) => process.stderr.write(
699
+ `${error instanceof Error ? error.message : "Inventory sync failed"}
700
+ `
701
+ )
702
+ ),
703
+ 15 * 6e4
704
+ );
705
+ }
706
+ async function runManaged(command, args) {
707
+ if (!command) throw new Error("Use: simplr-connect run -- <ai-command> [arguments]");
708
+ const releaseLock = await acquireSupervisorLock();
709
+ const state = await loadState();
710
+ const child = spawn(command, args, { cwd: process.cwd(), stdio: "inherit", shell: false, detached: platform() !== "win32" });
711
+ if (!child.pid) throw new Error("Managed AI process could not be started");
712
+ const exit = new Promise((resolve, reject) => {
713
+ child.once("error", reject);
714
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
715
+ });
716
+ const managed = {
717
+ child,
718
+ agent: basename(command),
719
+ label: `Managed ${basename(command)} process`,
720
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
721
+ paused: false
722
+ };
723
+ managedProcesses.set(child.pid, managed);
724
+ child.once("exit", () => managedProcesses.delete(child.pid));
725
+ await sync(state);
726
+ const heartbeatTimer = setInterval(() => void heartbeat(state).catch((error) => process.stderr.write(`${error instanceof Error ? error.message : "Heartbeat failed"}
727
+ `)), 6e4);
728
+ const inventoryTimer = setInterval(() => void sync(state).catch((error) => process.stderr.write(`${error instanceof Error ? error.message : "Inventory sync failed"}
729
+ `)), 15 * 6e4);
730
+ const exitCode = await exit;
731
+ clearInterval(heartbeatTimer);
732
+ clearInterval(inventoryTimer);
733
+ state.control_state = "running";
734
+ await saveState(state);
735
+ await sync(state).catch(() => void 0);
736
+ releaseLock();
737
+ process.exitCode = exitCode;
738
+ }
739
+ function argument(name) {
740
+ const index = process.argv.indexOf(name);
741
+ return index >= 0 ? process.argv[index + 1] : void 0;
742
+ }
743
+ async function main() {
744
+ const command = process.argv[2];
745
+ if (command === "enroll") {
746
+ const code = argument("--code");
747
+ const apiUrlOverride = argument("--api-url");
748
+ if (!code)
749
+ throw new Error("Use: simplr-connect enroll --code <one-time-code>");
750
+ await enroll(code, apiUrlOverride);
751
+ return;
752
+ }
753
+ if (command === "sync") {
754
+ await sync();
755
+ return;
756
+ }
757
+ if (command === "watch") {
758
+ await watch();
759
+ return;
760
+ }
761
+ if (command === "run") {
762
+ const separator = process.argv.indexOf("--");
763
+ const managedCommand = separator >= 0 ? process.argv[separator + 1] : process.argv[3];
764
+ const args = separator >= 0 ? process.argv.slice(separator + 2) : process.argv.slice(4);
765
+ await runManaged(managedCommand || "", args);
766
+ return;
767
+ }
768
+ process.stdout.write(
769
+ "Simplr Connect\n\nCommands:\n enroll --api-url <url> --code <code>\n sync\n watch\n run -- <ai-command> [arguments]\n"
770
+ );
771
+ }
772
+ main().catch((error) => {
773
+ process.stderr.write(
774
+ `${error instanceof Error ? error.message : "Simplr Connect failed"}
775
+ `
776
+ );
777
+ process.exitCode = 1;
778
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@simplr-ai/connect",
3
+ "version": "0.3.0-dev.0",
4
+ "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "simplr-connect": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/",
16
+ "provenance": true
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/doshexchnage/simplr-sdk.git",
21
+ "directory": "simplr-connect"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/doshexchnage/simplr-sdk/issues"
25
+ },
26
+ "homepage": "https://github.com/doshexchnage/simplr-sdk/tree/main/simplr-connect#readme",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "prepack": "bun run build",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.6.0",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "keywords": [
41
+ "simplr",
42
+ "codex",
43
+ "claude",
44
+ "mcp",
45
+ "workstation"
46
+ ],
47
+ "license": "UNLICENSED"
48
+ }