@onklave/agent-cli 0.1.48 → 0.1.50

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.
Files changed (2) hide show
  1. package/main.js +1005 -74
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -8,9 +8,24 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
 
9
9
  // _apps/@onklave/agent-cli/src/services/credential-store.ts
10
10
  import * as fs from "fs";
11
- import * as path from "path";
12
- import * as os from "os";
11
+ import * as path2 from "path";
13
12
  import { createRequire } from "module";
13
+
14
+ // _apps/@onklave/agent-cli/src/services/config-paths.ts
15
+ import * as os from "os";
16
+ import * as path from "path";
17
+ function resolveConfigDir() {
18
+ const override = process.env["ONKLAVE_CONFIG_DIR"];
19
+ if (override && override.trim()) {
20
+ return override.trim();
21
+ }
22
+ return path.join(os.homedir(), ".config", "onklave");
23
+ }
24
+ function credentialsFilePath() {
25
+ return path.join(resolveConfigDir(), "credentials.json");
26
+ }
27
+
28
+ // _apps/@onklave/agent-cli/src/services/credential-store.ts
14
29
  var nodeRequire = createRequire(import.meta.url);
15
30
  var KEYTAR_SERVICE = "onklave-agent-cli";
16
31
  var KEYTAR_ACCOUNT = "default";
@@ -33,13 +48,13 @@ function warnFallback(reason) {
33
48
  }
34
49
  warnedFallback = true;
35
50
  console.warn(
36
- `[onklave] OS keychain unavailable (${reason}); falling back to file store at ~/.config/onklave/credentials.json`
51
+ `[onklave] OS keychain unavailable (${reason}); falling back to file store at ${credentialsFilePath()}`
37
52
  );
38
53
  }
39
54
  var CredentialStore = class {
40
55
  constructor() {
41
- this.configDir = path.join(os.homedir(), ".config", "onklave");
42
- this.credentialsPath = path.join(this.configDir, "credentials.json");
56
+ this.configDir = resolveConfigDir();
57
+ this.credentialsPath = path2.join(this.configDir, "credentials.json");
43
58
  }
44
59
  /*
45
60
  * Read the full credentials object, keychain-first.
@@ -364,7 +379,7 @@ async function whoamiCommand() {
364
379
 
365
380
  // _apps/@onklave/agent-cli/src/services/config-resolver.ts
366
381
  import * as fs2 from "fs";
367
- import * as path2 from "path";
382
+ import * as path3 from "path";
368
383
  import * as os2 from "os";
369
384
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
370
385
  var DEFAULT_TIMEOUT = 120;
@@ -456,7 +471,7 @@ var ConfigResolver = class {
456
471
  const fromFlags = {};
457
472
  if (typeof flags["task"] === "string") fromFlags.task = flags["task"];
458
473
  if (typeof flags["context"] === "string")
459
- fromFlags.context = path2.resolve(flags["context"]);
474
+ fromFlags.context = path3.resolve(flags["context"]);
460
475
  if (typeof flags["persona"] === "string")
461
476
  fromFlags.persona = flags["persona"];
462
477
  if (typeof flags["workflow"] === "string")
@@ -482,7 +497,7 @@ var ConfigResolver = class {
482
497
  * Load .onklave.json from the given directory.
483
498
  */
484
499
  loadOnklaveJson(cwd) {
485
- const filePath = path2.join(cwd, ".onklave.json");
500
+ const filePath = path3.join(cwd, ".onklave.json");
486
501
  if (!fs2.existsSync(filePath)) {
487
502
  return null;
488
503
  }
@@ -500,7 +515,7 @@ var ConfigResolver = class {
500
515
  * Load global config from ~/.config/onklave/config.json.
501
516
  */
502
517
  loadGlobalConfig() {
503
- const filePath = path2.join(
518
+ const filePath = path3.join(
504
519
  os2.homedir(),
505
520
  ".config",
506
521
  "onklave",
@@ -520,8 +535,8 @@ var ConfigResolver = class {
520
535
  * Save a value to the global config file.
521
536
  */
522
537
  saveGlobalConfig(key, value) {
523
- const configDir = path2.join(os2.homedir(), ".config", "onklave");
524
- const filePath = path2.join(configDir, "config.json");
538
+ const configDir = path3.join(os2.homedir(), ".config", "onklave");
539
+ const filePath = path3.join(configDir, "config.json");
525
540
  if (!fs2.existsSync(configDir)) {
526
541
  fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
527
542
  }
@@ -774,8 +789,8 @@ var PlatformClient = class {
774
789
  /*
775
790
  * Make an authenticated HTTP request to the platform.
776
791
  */
777
- async request(method, path12, body, extraHeaders) {
778
- const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path12}`;
792
+ async request(method, path14, body, extraHeaders) {
793
+ const url = `${this.baseUrl}${GATEWAY_SERVICE_PREFIX}${path14}`;
779
794
  const headers = {
780
795
  Authorization: `Bearer ${this.token}`,
781
796
  "Content-Type": "application/json",
@@ -1250,33 +1265,33 @@ var AuditStreamer = class {
1250
1265
 
1251
1266
  // _apps/@onklave/agent-cli/src/services/guardrail-enforcer.ts
1252
1267
  import * as fs3 from "fs";
1253
- import * as path3 from "path";
1268
+ import * as path4 from "path";
1254
1269
  function safeRealpath(target) {
1255
- let current = path3.resolve(target);
1270
+ let current = path4.resolve(target);
1256
1271
  const tail = [];
1257
1272
  for (let i = 0; i < 4096; i++) {
1258
1273
  try {
1259
1274
  const real = fs3.realpathSync(current);
1260
- return tail.length ? path3.join(real, ...tail.reverse()) : real;
1275
+ return tail.length ? path4.join(real, ...tail.reverse()) : real;
1261
1276
  } catch {
1262
- const parent = path3.dirname(current);
1277
+ const parent = path4.dirname(current);
1263
1278
  if (parent === current) break;
1264
- tail.push(path3.basename(current));
1279
+ tail.push(path4.basename(current));
1265
1280
  current = parent;
1266
1281
  }
1267
1282
  }
1268
- return path3.resolve(target);
1283
+ return path4.resolve(target);
1269
1284
  }
1270
1285
  function isPathWithin(target, root) {
1271
1286
  const realRoot = safeRealpath(root);
1272
1287
  const realTarget = safeRealpath(target);
1273
1288
  if (realTarget === realRoot) return true;
1274
- const rootWithSep = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
1289
+ const rootWithSep = realRoot.endsWith(path4.sep) ? realRoot : realRoot + path4.sep;
1275
1290
  return realTarget.startsWith(rootWithSep);
1276
1291
  }
1277
1292
  function checkWorkspaceAccess(contextPath, allowedRoots) {
1278
1293
  const real = safeRealpath(contextPath);
1279
- if (real === path3.parse(real).root) {
1294
+ if (real === path4.parse(real).root) {
1280
1295
  return {
1281
1296
  allowed: false,
1282
1297
  reason: `Refusing to run with the filesystem root "${real}" as the workspace.`
@@ -1387,8 +1402,8 @@ var GuardrailEnforcer = class {
1387
1402
  "id_rsa",
1388
1403
  "id_ed25519"
1389
1404
  ];
1390
- const basename3 = path3.basename(realPath);
1391
- const normForward = realPath.split(path3.sep).join("/");
1405
+ const basename3 = path4.basename(realPath);
1406
+ const normForward = realPath.split(path4.sep).join("/");
1392
1407
  for (const sensitive of sensitivePatterns) {
1393
1408
  if (basename3 === sensitive || normForward.includes(`/${sensitive}`)) {
1394
1409
  return {
@@ -1530,7 +1545,7 @@ var HeartbeatService = class {
1530
1545
 
1531
1546
  // _apps/@onklave/agent-cli/src/services/cli-version.ts
1532
1547
  import * as fs4 from "node:fs";
1533
- import * as path4 from "node:path";
1548
+ import * as path5 from "node:path";
1534
1549
  import { fileURLToPath } from "node:url";
1535
1550
 
1536
1551
  // _apps/@onklave/agent-cli/src/services/daemon-update-checker.service.ts
@@ -1621,14 +1636,14 @@ function parseSemverNumeric(v) {
1621
1636
  // _apps/@onklave/agent-cli/src/services/cli-version.ts
1622
1637
  function readPackageVersion() {
1623
1638
  try {
1624
- const moduleDir = path4.dirname(fileURLToPath(import.meta.url));
1639
+ const moduleDir = path5.dirname(fileURLToPath(import.meta.url));
1625
1640
  const candidates = [
1626
- path4.join(moduleDir, "package.json"),
1641
+ path5.join(moduleDir, "package.json"),
1627
1642
  // bundled: dist root
1628
- path4.join(moduleDir, "..", "package.json"),
1629
- path4.join(moduleDir, "..", "..", "package.json"),
1643
+ path5.join(moduleDir, "..", "package.json"),
1644
+ path5.join(moduleDir, "..", "..", "package.json"),
1630
1645
  // dev: src/services -> root
1631
- path4.join(moduleDir, "..", "..", "..", "package.json")
1646
+ path5.join(moduleDir, "..", "..", "..", "package.json")
1632
1647
  ];
1633
1648
  for (const p of candidates) {
1634
1649
  if (!fs4.existsSync(p)) continue;
@@ -1661,7 +1676,7 @@ function collectPosture() {
1661
1676
  }
1662
1677
 
1663
1678
  // _apps/@onklave/agent-cli/src/commands/run.command.ts
1664
- import * as path5 from "path";
1679
+ import * as path6 from "path";
1665
1680
 
1666
1681
  // _apps/@onklave/agent-cli/src/tui/render.tsx
1667
1682
  import { render } from "ink";
@@ -2361,7 +2376,7 @@ async function runCommand(args) {
2361
2376
  }
2362
2377
  const addDirs = [
2363
2378
  .../* @__PURE__ */ new Set([
2364
- path5.resolve(resolvedConfig.context),
2379
+ path6.resolve(resolvedConfig.context),
2365
2380
  ...effectiveAllowedRoots
2366
2381
  ])
2367
2382
  ];
@@ -2825,7 +2840,7 @@ async function denyCommand(args) {
2825
2840
 
2826
2841
  // _apps/@onklave/agent-cli/src/commands/doctor.command.ts
2827
2842
  import * as fs6 from "fs";
2828
- import * as path6 from "path";
2843
+ import * as path7 from "path";
2829
2844
 
2830
2845
  // _apps/@onklave/agent-cli/src/services/command-exists.ts
2831
2846
  import { execSync } from "child_process";
@@ -2955,7 +2970,7 @@ function checkNodeVersion() {
2955
2970
  };
2956
2971
  }
2957
2972
  function checkProjectConfig() {
2958
- const configPath = path6.join(process.cwd(), ".onklave.json");
2973
+ const configPath = path7.join(process.cwd(), ".onklave.json");
2959
2974
  if (fs6.existsSync(configPath)) {
2960
2975
  try {
2961
2976
  const raw = fs6.readFileSync(configPath, "utf-8");
@@ -3009,9 +3024,9 @@ async function checkWebSocket() {
3009
3024
 
3010
3025
  // _apps/@onklave/agent-cli/src/commands/init.command.ts
3011
3026
  import * as fs7 from "fs";
3012
- import * as path7 from "path";
3027
+ import * as path8 from "path";
3013
3028
  async function initCommand() {
3014
- const configPath = path7.join(process.cwd(), ".onklave.json");
3029
+ const configPath = path8.join(process.cwd(), ".onklave.json");
3015
3030
  if (fs7.existsSync(configPath)) {
3016
3031
  console.error("Error: .onklave.json already exists in this directory.");
3017
3032
  console.log("To overwrite, delete the existing file first.");
@@ -3019,7 +3034,7 @@ async function initCommand() {
3019
3034
  return;
3020
3035
  }
3021
3036
  const template = {
3022
- project: path7.basename(process.cwd()),
3037
+ project: path8.basename(process.cwd()),
3023
3038
  org: "",
3024
3039
  description: "",
3025
3040
  defaults: {
@@ -3279,8 +3294,9 @@ async function logsCommand(args) {
3279
3294
  }
3280
3295
 
3281
3296
  // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
3282
- import * as fs10 from "fs";
3283
- import * as path10 from "path";
3297
+ import * as fs11 from "fs";
3298
+ import * as path12 from "path";
3299
+ import * as os7 from "os";
3284
3300
 
3285
3301
  // _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
3286
3302
  var DaemonCommsService = class {
@@ -3707,8 +3723,8 @@ var PlatformBrokerClient = class {
3707
3723
  params
3708
3724
  );
3709
3725
  }
3710
- async request(method, path12, body) {
3711
- const url = `${this.baseUrl}/agent-orchestration${path12}`;
3726
+ async request(method, path14, body) {
3727
+ const url = `${this.baseUrl}/agent-orchestration${path14}`;
3712
3728
  const response = await fetch(url, {
3713
3729
  method,
3714
3730
  headers: {
@@ -3744,20 +3760,28 @@ var PlatformBrokerClient = class {
3744
3760
 
3745
3761
  // _apps/@onklave/agent-cli/src/services/work-item-runner.service.ts
3746
3762
  import { spawn as spawn2 } from "child_process";
3763
+ import * as crypto from "crypto";
3747
3764
  import * as fs8 from "fs";
3748
3765
  import * as os5 from "os";
3749
- import * as path8 from "path";
3766
+ import * as path9 from "path";
3750
3767
  var WorkItemRunner = class {
3751
3768
  constructor(opts = {}) {
3769
+ /**
3770
+ * Per-cache-dir promise chain serialising the short git prepare/cleanup
3771
+ * sections so concurrent tasks on the same repo don't race on the shared
3772
+ * `.git` (worktree add/remove, fetch). Keyed by absolute cache dir.
3773
+ */
3774
+ this.repoLocks = /* @__PURE__ */ new Map();
3752
3775
  this.sessionManager = opts.sessionManager ?? new SessionManager();
3753
3776
  this.git = opts.git ?? defaultGit;
3754
3777
  this.model = opts.model ?? "claude-sonnet-4-6";
3755
3778
  this.timeoutSeconds = opts.timeoutSeconds ?? 1800;
3779
+ this.cacheRoot = opts.cacheRoot ?? path9.join(os5.homedir(), ".onklave", "clones");
3756
3780
  }
3757
3781
  /**
3758
- * Clone branch → run Claude Code. Returns the run outcome; never throws —
3759
- * failures are mapped to a `failed` result with an error summary so the
3760
- * caller can always report a status back to the broker.
3782
+ * Prepare workspace → run Claude Code → tear down. Returns the run outcome;
3783
+ * never throws — failures are mapped to a `failed` result with an error
3784
+ * summary so the caller can always report a status back to the broker.
3761
3785
  */
3762
3786
  async run(sessionId, workItem) {
3763
3787
  const repo = workItem.project?.repos?.[0];
@@ -3768,18 +3792,19 @@ var WorkItemRunner = class {
3768
3792
  };
3769
3793
  }
3770
3794
  const workDir = fs8.mkdtempSync(
3771
- path8.join(os5.tmpdir(), `onklave-pickup-${sessionId}-`)
3795
+ path9.join(os5.tmpdir(), `onklave-pickup-${sessionId}-`)
3772
3796
  );
3773
- const repoDir = path8.join(workDir, sanitizeName(repo.name) || "repo");
3797
+ const repoDir = path9.join(workDir, sanitizeName(repo.name) || "repo");
3774
3798
  const branchName = `onklave/work-item/${workItem.id}`;
3799
+ const repoUrl = repo.url;
3800
+ let cacheDir;
3775
3801
  try {
3776
- await this.git(["clone", repo.url, repoDir], workDir);
3777
- await this.git(["checkout", "-b", branchName], repoDir);
3802
+ cacheDir = await this.prepareWorkspace(repoUrl, repoDir, branchName);
3778
3803
  } catch (err) {
3779
3804
  cleanup(workDir);
3780
3805
  return {
3781
3806
  status: "failed",
3782
- summary: `Failed to prepare workspace for ${repo.url}: ${err.message}`,
3807
+ summary: `Failed to prepare workspace for ${repoUrl}: ${err.message}`,
3783
3808
  branchName
3784
3809
  };
3785
3810
  }
@@ -3792,7 +3817,7 @@ var WorkItemRunner = class {
3792
3817
  });
3793
3818
  const writeCheck = guardrails.checkPathAccess(repoDir, "write");
3794
3819
  if (!writeCheck.allowed) {
3795
- cleanup(workDir);
3820
+ await this.cleanupWorkspace(workDir, repoDir, cacheDir, branchName);
3796
3821
  return {
3797
3822
  status: "failed",
3798
3823
  summary: `Guardrails blocked the workspace: ${writeCheck.reason}`,
@@ -3839,7 +3864,7 @@ var WorkItemRunner = class {
3839
3864
  }
3840
3865
  });
3841
3866
  });
3842
- cleanup(workDir);
3867
+ await this.cleanupWorkspace(workDir, repoDir, cacheDir, branchName);
3843
3868
  if (exitCode === 0) {
3844
3869
  return {
3845
3870
  status: "in_review",
@@ -3854,6 +3879,89 @@ var WorkItemRunner = class {
3854
3879
  branchName
3855
3880
  };
3856
3881
  }
3882
+ /**
3883
+ * Lay down a working tree for the task at `repoDir` on a fresh `branchName`.
3884
+ *
3885
+ * Fast path: ensure a persistent clone exists for the repo (clone once, fetch
3886
+ * thereafter) and add a throwaway worktree for this task. Returns the cache
3887
+ * dir so teardown can remove the worktree + branch.
3888
+ *
3889
+ * Fallback: on any failure of the cached path, do the original
3890
+ * fresh-clone-then-branch into `repoDir` and return null (nothing cached to
3891
+ * unwind). This keeps the optimization strictly non-regressing.
3892
+ */
3893
+ async prepareWorkspace(repoUrl, repoDir, branchName) {
3894
+ const cacheDir = this.cacheDirFor(repoUrl);
3895
+ try {
3896
+ await this.withRepoLock(
3897
+ cacheDir,
3898
+ () => this.prepareViaWorktree(repoUrl, cacheDir, repoDir, branchName)
3899
+ );
3900
+ return cacheDir;
3901
+ } catch {
3902
+ cleanup(repoDir);
3903
+ await this.git(["clone", repoUrl, repoDir], path9.dirname(repoDir));
3904
+ await this.git(["checkout", "-b", branchName], repoDir);
3905
+ return null;
3906
+ }
3907
+ }
3908
+ /** Ensure the cache clone exists/up-to-date, then add a task worktree. */
3909
+ async prepareViaWorktree(repoUrl, cacheDir, taskDir, branchName) {
3910
+ if (fs8.existsSync(path9.join(cacheDir, ".git"))) {
3911
+ await this.git(["fetch", "--prune", "origin"], cacheDir);
3912
+ } else {
3913
+ cleanup(cacheDir);
3914
+ fs8.mkdirSync(path9.dirname(cacheDir), { recursive: true });
3915
+ await this.git(["clone", repoUrl, cacheDir], path9.dirname(cacheDir));
3916
+ }
3917
+ await this.git(["worktree", "prune"], cacheDir);
3918
+ await this.gitIgnoreError(["branch", "-D", branchName], cacheDir);
3919
+ await this.git(
3920
+ ["worktree", "add", "--force", "-b", branchName, taskDir, "origin/HEAD"],
3921
+ cacheDir
3922
+ );
3923
+ }
3924
+ /**
3925
+ * Tear down a task workspace. For the worktree path this removes the linked
3926
+ * worktree and deletes its branch from the cache (keeping the cache clone for
3927
+ * reuse); for the fresh-clone path (`cacheDir === null`) it just removes the
3928
+ * temp dir. Always best-effort — teardown failures never fail the run.
3929
+ */
3930
+ async cleanupWorkspace(workDir, taskDir, cacheDir, branchName) {
3931
+ if (cacheDir) {
3932
+ await this.withRepoLock(cacheDir, async () => {
3933
+ await this.gitIgnoreError(
3934
+ ["worktree", "remove", "--force", taskDir],
3935
+ cacheDir
3936
+ );
3937
+ await this.gitIgnoreError(["worktree", "prune"], cacheDir);
3938
+ await this.gitIgnoreError(["branch", "-D", branchName], cacheDir);
3939
+ });
3940
+ }
3941
+ cleanup(workDir);
3942
+ }
3943
+ /** Absolute cache dir for a repo URL — stable hash, collision-safe enough. */
3944
+ cacheDirFor(repoUrl) {
3945
+ const hash = crypto.createHash("sha1").update(repoUrl).digest("hex").slice(0, 16);
3946
+ return path9.join(this.cacheRoot, hash);
3947
+ }
3948
+ /** Run a git command, swallowing a non-zero exit (best-effort cleanup). */
3949
+ gitIgnoreError(args, cwd) {
3950
+ return this.git(args, cwd).catch(() => void 0);
3951
+ }
3952
+ /** Serialise an async section against others sharing the same key. */
3953
+ withRepoLock(key, fn) {
3954
+ const prev = this.repoLocks.get(key) ?? Promise.resolve();
3955
+ const next = prev.then(fn, fn);
3956
+ this.repoLocks.set(
3957
+ key,
3958
+ next.then(
3959
+ () => void 0,
3960
+ () => void 0
3961
+ )
3962
+ );
3963
+ return next;
3964
+ }
3857
3965
  };
3858
3966
  function buildTask(workItem) {
3859
3967
  const parts = [workItem.title];
@@ -4195,8 +4303,7 @@ var DaemonUpgradeService = class {
4195
4303
 
4196
4304
  // _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
4197
4305
  import * as fs9 from "fs";
4198
- import * as path9 from "path";
4199
- import * as os7 from "os";
4306
+ import * as path10 from "path";
4200
4307
  var VALID_TRANSITIONS = {
4201
4308
  installing: ["registered"],
4202
4309
  registered: ["starting"],
@@ -4207,10 +4314,10 @@ var VALID_TRANSITIONS = {
4207
4314
  stopped: ["starting"]
4208
4315
  };
4209
4316
  function defaultStateFilePath() {
4210
- return path9.join(os7.homedir(), ".config", "onklave", "daemon.state.json");
4317
+ return path10.join(resolveConfigDir(), "daemon.state.json");
4211
4318
  }
4212
4319
  function defaultPidFilePath() {
4213
- return path9.join(os7.homedir(), ".config", "onklave", "daemon.pid");
4320
+ return path10.join(resolveConfigDir(), "daemon.pid");
4214
4321
  }
4215
4322
  var DaemonStateError = class extends Error {
4216
4323
  constructor(message) {
@@ -4285,7 +4392,7 @@ var DaemonStateService = class {
4285
4392
  }
4286
4393
  }
4287
4394
  persist(reason) {
4288
- const dir = path9.dirname(this.stateFile);
4395
+ const dir = path10.dirname(this.stateFile);
4289
4396
  fs9.mkdirSync(dir, { recursive: true });
4290
4397
  const payload = {
4291
4398
  state: this.current,
@@ -4428,6 +4535,568 @@ function emitUnitFile(flavour) {
4428
4535
  }
4429
4536
  }
4430
4537
 
4538
+ // _apps/@onklave/agent-cli/src/services/daemon-installer.service.ts
4539
+ import * as fs10 from "fs";
4540
+ import * as path11 from "path";
4541
+ import { execFileSync } from "child_process";
4542
+ var LAUNCHD_LABEL = "app.onklave.daemon";
4543
+ var SYSTEMD_UNIT_NAME = "onklave-daemon";
4544
+ var WINDOWS_TASK_NAME = "OnklaveDaemon";
4545
+ var SYSTEMD_SYSTEM_UNIT_PATH = `/etc/systemd/system/${SYSTEMD_UNIT_NAME}.service`;
4546
+ var LINUX_SERVICE_USER = "onklave";
4547
+ var WINDOWS_SERVICE_ACCOUNT = "NT AUTHORITY\\LocalService";
4548
+ function xmlEscape(value) {
4549
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4550
+ }
4551
+ function macPlistPath(homeDir) {
4552
+ return path11.join(
4553
+ homeDir,
4554
+ "Library",
4555
+ "LaunchAgents",
4556
+ `${LAUNCHD_LABEL}.plist`
4557
+ );
4558
+ }
4559
+ function macLogDir(homeDir) {
4560
+ return path11.join(homeDir, "Library", "Logs", "onklave");
4561
+ }
4562
+ function macPlist(inp) {
4563
+ const logDir = macLogDir(inp.homeDir);
4564
+ return `<?xml version="1.0" encoding="UTF-8"?>
4565
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4566
+ <plist version="1.0">
4567
+ <dict>
4568
+ <key>Label</key>
4569
+ <string>${LAUNCHD_LABEL}</string>
4570
+ <key>ProgramArguments</key>
4571
+ <array>
4572
+ <string>${inp.nodePath}</string>
4573
+ <string>${inp.cliPath}</string>
4574
+ <string>daemon</string>
4575
+ </array>
4576
+ <key>RunAtLoad</key>
4577
+ <true/>
4578
+ <key>KeepAlive</key>
4579
+ <dict>
4580
+ <key>SuccessfulExit</key>
4581
+ <false/>
4582
+ <key>Crashed</key>
4583
+ <true/>
4584
+ </dict>
4585
+ <key>ThrottleInterval</key>
4586
+ <integer>10</integer>
4587
+ <key>ProcessType</key>
4588
+ <string>Background</string>
4589
+ <key>StandardOutPath</key>
4590
+ <string>${path11.join(logDir, "daemon.out.log")}</string>
4591
+ <key>StandardErrorPath</key>
4592
+ <string>${path11.join(logDir, "daemon.err.log")}</string>
4593
+ </dict>
4594
+ </plist>
4595
+ `;
4596
+ }
4597
+ function macInstallPlan(inp) {
4598
+ const plistPath = macPlistPath(inp.homeDir);
4599
+ const domainTarget = `gui/${inp.uid ?? ""}`;
4600
+ const serviceTarget = `${domainTarget}/${LAUNCHD_LABEL}`;
4601
+ const commands = [
4602
+ // Tear down any prior agent so bootstrap doesn't fail on a stale label.
4603
+ { argv: ["launchctl", "bootout", serviceTarget], optional: true }
4604
+ ];
4605
+ if (inp.start) {
4606
+ commands.push({
4607
+ argv: ["launchctl", "bootstrap", domainTarget, plistPath]
4608
+ });
4609
+ commands.push({ argv: ["launchctl", "kickstart", "-k", serviceTarget] });
4610
+ }
4611
+ return {
4612
+ platform: "darwin",
4613
+ manager: "launchd",
4614
+ serviceLabel: LAUNCHD_LABEL,
4615
+ files: [{ filePath: plistPath, contents: macPlist(inp) }],
4616
+ commands,
4617
+ ensureDirs: [macLogDir(inp.homeDir)],
4618
+ startsNow: inp.start
4619
+ };
4620
+ }
4621
+ function macUninstallPlan(homeDir, uid) {
4622
+ const serviceTarget = `gui/${uid ?? ""}/${LAUNCHD_LABEL}`;
4623
+ return {
4624
+ platform: "darwin",
4625
+ manager: "launchd",
4626
+ serviceLabel: LAUNCHD_LABEL,
4627
+ removeFiles: [macPlistPath(homeDir)],
4628
+ commands: [
4629
+ { argv: ["launchctl", "bootout", serviceTarget], optional: true }
4630
+ ]
4631
+ };
4632
+ }
4633
+ function systemdUnitPath(homeDir) {
4634
+ return path11.join(
4635
+ homeDir,
4636
+ ".config",
4637
+ "systemd",
4638
+ "user",
4639
+ `${SYSTEMD_UNIT_NAME}.service`
4640
+ );
4641
+ }
4642
+ function systemdUnit(inp) {
4643
+ return `[Unit]
4644
+ Description=Onklave Agent CLI Daemon
4645
+ Documentation=https://docs.onklave.app/cli/daemon
4646
+ After=network-online.target
4647
+ Wants=network-online.target
4648
+
4649
+ [Service]
4650
+ Type=simple
4651
+ ExecStart=${inp.nodePath} ${inp.cliPath} daemon
4652
+ ExecStop=${inp.nodePath} ${inp.cliPath} daemon drain
4653
+ Restart=on-failure
4654
+ RestartSec=10s
4655
+ StartLimitIntervalSec=300
4656
+ StartLimitBurst=5
4657
+ KillSignal=SIGTERM
4658
+ TimeoutStopSec=1830s
4659
+
4660
+ [Install]
4661
+ WantedBy=default.target
4662
+ `;
4663
+ }
4664
+ function linuxInstallPlan(inp) {
4665
+ const commands = [
4666
+ { argv: ["systemctl", "--user", "daemon-reload"] },
4667
+ // enable --now starts + enables; enable (no --now) just enables for login.
4668
+ {
4669
+ argv: inp.start ? ["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT_NAME] : ["systemctl", "--user", "enable", SYSTEMD_UNIT_NAME]
4670
+ }
4671
+ ];
4672
+ return {
4673
+ platform: "linux",
4674
+ manager: "systemd (user)",
4675
+ serviceLabel: SYSTEMD_UNIT_NAME,
4676
+ files: [
4677
+ { filePath: systemdUnitPath(inp.homeDir), contents: systemdUnit(inp) }
4678
+ ],
4679
+ commands,
4680
+ ensureDirs: [],
4681
+ startsNow: inp.start
4682
+ };
4683
+ }
4684
+ function linuxUninstallPlan(homeDir) {
4685
+ return {
4686
+ platform: "linux",
4687
+ manager: "systemd (user)",
4688
+ serviceLabel: SYSTEMD_UNIT_NAME,
4689
+ removeFiles: [systemdUnitPath(homeDir)],
4690
+ commands: [
4691
+ {
4692
+ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT_NAME],
4693
+ optional: true
4694
+ },
4695
+ { argv: ["systemctl", "--user", "daemon-reload"], optional: true }
4696
+ ]
4697
+ };
4698
+ }
4699
+ function windowsTaskXmlPath(homeDir) {
4700
+ return path11.join(homeDir, ".config", "onklave", `${WINDOWS_TASK_NAME}.xml`);
4701
+ }
4702
+ function windowsLogPath(homeDir) {
4703
+ return path11.join(homeDir, ".config", "onklave", "logs", "daemon.log");
4704
+ }
4705
+ function windowsTaskXml(inp) {
4706
+ const user = inp.windowsUser ?? "";
4707
+ const logPath = windowsLogPath(inp.homeDir);
4708
+ const innerCmd = `""${inp.nodePath}" "${inp.cliPath}" daemon >> "${logPath}" 2>&1"`;
4709
+ return `<?xml version="1.0" encoding="UTF-16"?>
4710
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
4711
+ <RegistrationInfo>
4712
+ <Description>Onklave Agent CLI Daemon</Description>
4713
+ <URI>\\${WINDOWS_TASK_NAME}</URI>
4714
+ </RegistrationInfo>
4715
+ <Triggers>
4716
+ <LogonTrigger>
4717
+ <Enabled>true</Enabled>
4718
+ <UserId>${xmlEscape(user)}</UserId>
4719
+ </LogonTrigger>
4720
+ </Triggers>
4721
+ <Principals>
4722
+ <Principal id="Author">
4723
+ <UserId>${xmlEscape(user)}</UserId>
4724
+ <LogonType>InteractiveToken</LogonType>
4725
+ <RunLevel>LeastPrivilege</RunLevel>
4726
+ </Principal>
4727
+ </Principals>
4728
+ <Settings>
4729
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
4730
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
4731
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
4732
+ <AllowHardTerminate>true</AllowHardTerminate>
4733
+ <StartWhenAvailable>true</StartWhenAvailable>
4734
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
4735
+ <IdleSettings>
4736
+ <StopOnIdleEnd>false</StopOnIdleEnd>
4737
+ <RestartOnIdle>false</RestartOnIdle>
4738
+ </IdleSettings>
4739
+ <AllowStartOnDemand>true</AllowStartOnDemand>
4740
+ <Enabled>true</Enabled>
4741
+ <Hidden>false</Hidden>
4742
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
4743
+ <RestartOnFailure>
4744
+ <Interval>PT1M</Interval>
4745
+ <Count>3</Count>
4746
+ </RestartOnFailure>
4747
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
4748
+ </Settings>
4749
+ <Actions Context="Author">
4750
+ <Exec>
4751
+ <Command>cmd.exe</Command>
4752
+ <Arguments>/c ${xmlEscape(innerCmd)}</Arguments>
4753
+ </Exec>
4754
+ </Actions>
4755
+ </Task>
4756
+ `;
4757
+ }
4758
+ function windowsInstallPlan(inp) {
4759
+ const xmlPath = windowsTaskXmlPath(inp.homeDir);
4760
+ const commands = [
4761
+ {
4762
+ argv: [
4763
+ "schtasks",
4764
+ "/create",
4765
+ "/tn",
4766
+ WINDOWS_TASK_NAME,
4767
+ "/xml",
4768
+ xmlPath,
4769
+ "/f"
4770
+ ]
4771
+ }
4772
+ ];
4773
+ if (inp.start) {
4774
+ commands.push({ argv: ["schtasks", "/run", "/tn", WINDOWS_TASK_NAME] });
4775
+ }
4776
+ return {
4777
+ platform: "win32",
4778
+ manager: "Task Scheduler",
4779
+ serviceLabel: WINDOWS_TASK_NAME,
4780
+ files: [{ filePath: xmlPath, contents: windowsTaskXml(inp) }],
4781
+ commands,
4782
+ ensureDirs: [path11.dirname(windowsLogPath(inp.homeDir))],
4783
+ startsNow: inp.start
4784
+ };
4785
+ }
4786
+ function windowsUninstallPlan(homeDir) {
4787
+ return {
4788
+ platform: "win32",
4789
+ manager: "Task Scheduler",
4790
+ serviceLabel: WINDOWS_TASK_NAME,
4791
+ removeFiles: [windowsTaskXmlPath(homeDir)],
4792
+ commands: [
4793
+ { argv: ["schtasks", "/end", "/tn", WINDOWS_TASK_NAME], optional: true },
4794
+ {
4795
+ argv: ["schtasks", "/delete", "/tn", WINDOWS_TASK_NAME, "/f"],
4796
+ optional: true
4797
+ }
4798
+ ]
4799
+ };
4800
+ }
4801
+ function systemCredFilePath(configDir) {
4802
+ return path11.join(configDir, "credentials.json");
4803
+ }
4804
+ function linuxSystemInstallPlan(inp) {
4805
+ const configDir = inp.systemConfigDir ?? "/var/lib/onklave";
4806
+ const unit = `[Unit]
4807
+ Description=Onklave Agent CLI Daemon
4808
+ Documentation=https://docs.onklave.app/cli/daemon
4809
+ After=network-online.target
4810
+ Wants=network-online.target
4811
+
4812
+ [Service]
4813
+ Type=simple
4814
+ User=${LINUX_SERVICE_USER}
4815
+ Group=${LINUX_SERVICE_USER}
4816
+ Environment=NODE_ENV=production
4817
+ Environment=ONKLAVE_CONFIG_DIR=${configDir}
4818
+ ExecStart=${inp.nodePath} ${inp.cliPath} daemon
4819
+ ExecStop=${inp.nodePath} ${inp.cliPath} daemon drain
4820
+ Restart=on-failure
4821
+ RestartSec=10s
4822
+ StartLimitIntervalSec=300
4823
+ StartLimitBurst=5
4824
+ KillSignal=SIGTERM
4825
+ TimeoutStopSec=1830s
4826
+
4827
+ # Hardening \u2014 the execution plane runs unprivileged (constitution \xA76).
4828
+ NoNewPrivileges=true
4829
+ ProtectSystem=strict
4830
+ ProtectHome=true
4831
+ PrivateTmp=true
4832
+ ReadWritePaths=${configDir}
4833
+
4834
+ [Install]
4835
+ WantedBy=multi-user.target
4836
+ `;
4837
+ const commands = [
4838
+ // Dedicated unprivileged service account. Idempotent: a re-run on a host
4839
+ // that already has the user fails here and is swallowed.
4840
+ {
4841
+ argv: [
4842
+ "useradd",
4843
+ "--system",
4844
+ "--no-create-home",
4845
+ "--shell",
4846
+ "/usr/sbin/nologin",
4847
+ LINUX_SERVICE_USER
4848
+ ],
4849
+ optional: true
4850
+ },
4851
+ // chown after the cred file is written (executor writes files before
4852
+ // commands) so the service account owns the dir + credentials.
4853
+ {
4854
+ argv: [
4855
+ "chown",
4856
+ "-R",
4857
+ `${LINUX_SERVICE_USER}:${LINUX_SERVICE_USER}`,
4858
+ configDir
4859
+ ]
4860
+ },
4861
+ { argv: ["chmod", "700", configDir] },
4862
+ { argv: ["systemctl", "daemon-reload"] },
4863
+ {
4864
+ argv: inp.start ? ["systemctl", "enable", "--now", SYSTEMD_UNIT_NAME] : ["systemctl", "enable", SYSTEMD_UNIT_NAME]
4865
+ }
4866
+ ];
4867
+ return {
4868
+ platform: "linux",
4869
+ manager: "systemd (system)",
4870
+ serviceLabel: SYSTEMD_UNIT_NAME,
4871
+ files: [
4872
+ { filePath: SYSTEMD_SYSTEM_UNIT_PATH, contents: unit, mode: 420 },
4873
+ {
4874
+ filePath: systemCredFilePath(configDir),
4875
+ contents: inp.credentialsJson ?? "",
4876
+ mode: 384
4877
+ }
4878
+ ],
4879
+ commands,
4880
+ ensureDirs: [configDir],
4881
+ startsNow: inp.start
4882
+ };
4883
+ }
4884
+ function windowsSystemInstallPlan(inp) {
4885
+ const configDir = inp.systemConfigDir ?? "C:\\ProgramData\\Onklave";
4886
+ const logDir = path11.join(configDir, "logs");
4887
+ const svc = WINDOWS_TASK_NAME;
4888
+ const env = `NODE_ENV=production ONKLAVE_CONFIG_DIR=${configDir}`;
4889
+ const commands = [
4890
+ // Idempotent: drop any prior service before re-installing.
4891
+ { argv: ["nssm", "stop", svc], optional: true },
4892
+ { argv: ["nssm", "remove", svc, "confirm"], optional: true },
4893
+ { argv: ["nssm", "install", svc, inp.nodePath, inp.cliPath, "daemon"] },
4894
+ { argv: ["nssm", "set", svc, "DisplayName", "Onklave Agent CLI Daemon"] },
4895
+ {
4896
+ argv: [
4897
+ "nssm",
4898
+ "set",
4899
+ svc,
4900
+ "Description",
4901
+ "Long-running agent worker for the Onklave platform"
4902
+ ]
4903
+ },
4904
+ { argv: ["nssm", "set", svc, "Start", "SERVICE_AUTO_START"] },
4905
+ // Unprivileged built-in account (not LocalSystem) — spec §security.
4906
+ { argv: ["nssm", "set", svc, "ObjectName", WINDOWS_SERVICE_ACCOUNT] },
4907
+ { argv: ["nssm", "set", svc, "AppEnvironmentExtra", env] },
4908
+ {
4909
+ argv: [
4910
+ "nssm",
4911
+ "set",
4912
+ svc,
4913
+ "AppStdout",
4914
+ path11.join(logDir, "daemon.out.log")
4915
+ ]
4916
+ },
4917
+ {
4918
+ argv: [
4919
+ "nssm",
4920
+ "set",
4921
+ svc,
4922
+ "AppStderr",
4923
+ path11.join(logDir, "daemon.err.log")
4924
+ ]
4925
+ },
4926
+ { argv: ["nssm", "set", svc, "AppExit", "Default", "Restart"] },
4927
+ { argv: ["nssm", "set", svc, "AppRestartDelay", "10000"] },
4928
+ // The daemon writes state + pid into the config dir and reads credentials
4929
+ // there, so LocalService needs modify on it.
4930
+ {
4931
+ argv: [
4932
+ "icacls",
4933
+ configDir,
4934
+ "/grant",
4935
+ `${WINDOWS_SERVICE_ACCOUNT}:(OI)(CI)(M)`
4936
+ ]
4937
+ }
4938
+ ];
4939
+ if (inp.start) {
4940
+ commands.push({ argv: ["nssm", "start", svc] });
4941
+ }
4942
+ return {
4943
+ platform: "win32",
4944
+ manager: "NSSM (service)",
4945
+ serviceLabel: svc,
4946
+ files: [
4947
+ {
4948
+ filePath: systemCredFilePath(configDir),
4949
+ contents: inp.credentialsJson ?? ""
4950
+ }
4951
+ ],
4952
+ commands,
4953
+ ensureDirs: [logDir],
4954
+ startsNow: inp.start
4955
+ };
4956
+ }
4957
+ function linuxSystemUninstallPlan(configDir) {
4958
+ return {
4959
+ platform: "linux",
4960
+ manager: "systemd (system)",
4961
+ serviceLabel: SYSTEMD_UNIT_NAME,
4962
+ removeFiles: [SYSTEMD_SYSTEM_UNIT_PATH, systemCredFilePath(configDir)],
4963
+ commands: [
4964
+ {
4965
+ argv: ["systemctl", "disable", "--now", SYSTEMD_UNIT_NAME],
4966
+ optional: true
4967
+ },
4968
+ { argv: ["systemctl", "daemon-reload"], optional: true }
4969
+ ]
4970
+ };
4971
+ }
4972
+ function windowsSystemUninstallPlan(configDir) {
4973
+ return {
4974
+ platform: "win32",
4975
+ manager: "NSSM (service)",
4976
+ serviceLabel: WINDOWS_TASK_NAME,
4977
+ removeFiles: [systemCredFilePath(configDir)],
4978
+ commands: [
4979
+ { argv: ["nssm", "stop", WINDOWS_TASK_NAME], optional: true },
4980
+ {
4981
+ argv: ["nssm", "remove", WINDOWS_TASK_NAME, "confirm"],
4982
+ optional: true
4983
+ }
4984
+ ]
4985
+ };
4986
+ }
4987
+ function buildInstallPlan(inp) {
4988
+ if (inp.system) {
4989
+ switch (inp.platform) {
4990
+ case "linux":
4991
+ return linuxSystemInstallPlan(inp);
4992
+ case "win32":
4993
+ return windowsSystemInstallPlan(inp);
4994
+ case "darwin":
4995
+ throw new Error(
4996
+ "--system is not supported on macOS; use per-user `onklave daemon install`."
4997
+ );
4998
+ }
4999
+ }
5000
+ switch (inp.platform) {
5001
+ case "darwin":
5002
+ return macInstallPlan(inp);
5003
+ case "linux":
5004
+ return linuxInstallPlan(inp);
5005
+ case "win32":
5006
+ return windowsInstallPlan(inp);
5007
+ }
5008
+ }
5009
+ function buildUninstallPlan(inp) {
5010
+ if (inp.system) {
5011
+ const configDir = inp.systemConfigDir ?? "";
5012
+ switch (inp.platform) {
5013
+ case "linux":
5014
+ return linuxSystemUninstallPlan(configDir);
5015
+ case "win32":
5016
+ return windowsSystemUninstallPlan(configDir);
5017
+ case "darwin":
5018
+ throw new Error(
5019
+ "--system is not supported on macOS; use per-user `onklave daemon uninstall`."
5020
+ );
5021
+ }
5022
+ }
5023
+ switch (inp.platform) {
5024
+ case "darwin":
5025
+ return macUninstallPlan(inp.homeDir, inp.uid);
5026
+ case "linux":
5027
+ return linuxUninstallPlan(inp.homeDir);
5028
+ case "win32":
5029
+ return windowsUninstallPlan(inp.homeDir);
5030
+ }
5031
+ }
5032
+ function runInstallPlan(plan) {
5033
+ for (const dir of plan.ensureDirs) {
5034
+ fs10.mkdirSync(dir, { recursive: true });
5035
+ }
5036
+ for (const file of plan.files) {
5037
+ fs10.mkdirSync(path11.dirname(file.filePath), { recursive: true });
5038
+ fs10.writeFileSync(
5039
+ file.filePath,
5040
+ file.contents,
5041
+ file.mode != null ? { mode: file.mode } : {}
5042
+ );
5043
+ }
5044
+ runCommands(plan.commands);
5045
+ }
5046
+ function runUninstallPlan(plan) {
5047
+ runCommands(plan.commands);
5048
+ for (const filePath of plan.removeFiles) {
5049
+ try {
5050
+ fs10.unlinkSync(filePath);
5051
+ } catch {
5052
+ }
5053
+ }
5054
+ }
5055
+ function runCommands(commands) {
5056
+ for (const cmd of commands) {
5057
+ const [bin, ...rest] = cmd.argv;
5058
+ try {
5059
+ execFileSync(bin, rest, { stdio: "pipe", timeout: 3e4 });
5060
+ } catch (err) {
5061
+ if (cmd.optional) continue;
5062
+ const detail = err.stderr?.toString().trim();
5063
+ throw new Error(
5064
+ `Command failed: ${cmd.argv.join(" ")}${detail ? `
5065
+ ${detail}` : ""}`
5066
+ );
5067
+ }
5068
+ }
5069
+ }
5070
+
5071
+ // _apps/@onklave/agent-cli/src/services/daemon-elevation.service.ts
5072
+ import { execSync as execSync2, execFileSync as execFileSync2 } from "child_process";
5073
+ function isElevated(platform2 = process.platform) {
5074
+ if (platform2 === "win32") {
5075
+ try {
5076
+ execSync2("net session", { stdio: "ignore", timeout: 5e3 });
5077
+ return true;
5078
+ } catch {
5079
+ return false;
5080
+ }
5081
+ }
5082
+ return typeof process.getuid === "function" && process.getuid() === 0;
5083
+ }
5084
+ function buildSudoReExec(nodePath, args) {
5085
+ return ["sudo", nodePath, ...args];
5086
+ }
5087
+ function psQuote(value) {
5088
+ return `'${value.replace(/'/g, "''")}'`;
5089
+ }
5090
+ function buildRunAsCommand(nodePath, args) {
5091
+ const argList = args.map(psQuote).join(",");
5092
+ const script = `Start-Process -FilePath ${psQuote(nodePath)} ` + (args.length ? `-ArgumentList ${argList} ` : "") + `-Verb RunAs -Wait`;
5093
+ return ["powershell", "-NoProfile", "-NonInteractive", "-Command", script];
5094
+ }
5095
+ function runElevated(argv) {
5096
+ const [bin, ...rest] = argv;
5097
+ execFileSync2(bin, rest, { stdio: "inherit" });
5098
+ }
5099
+
4431
5100
  // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
4432
5101
  var UNIT_FLAGS = {
4433
5102
  "--emit-systemd-unit": "systemd",
@@ -4446,10 +5115,12 @@ async function daemonCommand(args) {
4446
5115
  if (sub === "status") return daemonStatus();
4447
5116
  if (sub === "stop" || sub === "drain") return daemonStop();
4448
5117
  if (sub === "update") return daemonUpdate();
5118
+ if (sub === "install") return daemonInstall(args.slice(1));
5119
+ if (sub === "uninstall") return daemonUninstall(args.slice(1));
4449
5120
  if (sub && !sub.startsWith("--")) {
4450
5121
  console.error(`Unknown subcommand: ${sub}`);
4451
5122
  console.error(
4452
- "Usage: onklave daemon [status|stop|drain|update] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
5123
+ "Usage: onklave daemon [install|uninstall|status|stop|drain|update] [--system] [--no-start] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
4453
5124
  );
4454
5125
  process.exitCode = 1;
4455
5126
  return;
@@ -4628,12 +5299,12 @@ async function daemonStart() {
4628
5299
  allowAutoUpgrade: !!process.env["ONKLAVE_ALLOW_AUTO_UPGRADE"],
4629
5300
  currentVersion: readPackageVersion() ?? "0.0.0",
4630
5301
  runInstall: async () => {
4631
- const { execFileSync } = __require("child_process");
4632
- execFileSync("npm", ["install", "-g", "@onklave/agent-cli@latest"], {
5302
+ const { execFileSync: execFileSync3 } = __require("child_process");
5303
+ execFileSync3("npm", ["install", "-g", "@onklave/agent-cli@latest"], {
4633
5304
  stdio: "pipe",
4634
5305
  timeout: 18e4
4635
5306
  });
4636
- const v = execFileSync(
5307
+ const v = execFileSync3(
4637
5308
  "npm",
4638
5309
  ["view", "@onklave/agent-cli@latest", "version"],
4639
5310
  { stdio: "pipe", timeout: 3e4 }
@@ -4690,6 +5361,11 @@ async function daemonStart() {
4690
5361
  console.log(
4691
5362
  `Daemon online. machineId=${creds.machineId} pid=${process.pid}. Press Ctrl-C to drain.`
4692
5363
  );
5364
+ if (process.stdout.isTTY) {
5365
+ console.log(
5366
+ "Tip: run `onklave daemon install` to run this as a background service that auto-starts at login."
5367
+ );
5368
+ }
4693
5369
  await new Promise(() => void 0);
4694
5370
  }
4695
5371
  async function daemonStatus() {
@@ -4770,6 +5446,261 @@ async function daemonUpdate() {
4770
5446
  "Self-replace via npm provenance verification (spec \xA76.3) is deferred \u2014 see V6-CLI-002 Phase 7 in the kanban."
4771
5447
  );
4772
5448
  }
5449
+ function currentServicePlatform() {
5450
+ const p = process.platform;
5451
+ return p === "darwin" || p === "linux" || p === "win32" ? p : null;
5452
+ }
5453
+ function resolveCliPath() {
5454
+ return __require.main?.filename ?? process.argv[1];
5455
+ }
5456
+ function windowsPrincipalUser() {
5457
+ const domain = process.env["USERDOMAIN"];
5458
+ const user = process.env["USERNAME"] ?? "";
5459
+ return domain ? `${domain}\\${user}` : user;
5460
+ }
5461
+ function systemConfigDir(platform2) {
5462
+ if (platform2 === "win32") {
5463
+ return path12.join(
5464
+ process.env["ProgramData"] || "C:\\ProgramData",
5465
+ "Onklave"
5466
+ );
5467
+ }
5468
+ return "/var/lib/onklave";
5469
+ }
5470
+ function getFlagValue(args, flag) {
5471
+ const i = args.indexOf(flag);
5472
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : void 0;
5473
+ }
5474
+ async function daemonInstall(args) {
5475
+ const platform2 = currentServicePlatform();
5476
+ if (!platform2) {
5477
+ console.error(`Unsupported platform: ${process.platform}`);
5478
+ process.exitCode = 1;
5479
+ return;
5480
+ }
5481
+ const start = !args.includes("--no-start");
5482
+ if (args.includes("--system")) {
5483
+ return daemonInstallSystem(platform2, args, start);
5484
+ }
5485
+ return daemonInstallPerUser(platform2, start);
5486
+ }
5487
+ async function daemonInstallPerUser(platform2, start) {
5488
+ const creds = await new AuthService().getCredentials();
5489
+ if (!creds?.deviceToken || !creds?.machineId) {
5490
+ console.error(
5491
+ "Error: machine is not registered. Run: onklave register --token <bootstrap>"
5492
+ );
5493
+ process.exitCode = 1;
5494
+ return;
5495
+ }
5496
+ const plan = buildInstallPlan({
5497
+ platform: platform2,
5498
+ nodePath: process.execPath,
5499
+ cliPath: resolveCliPath(),
5500
+ homeDir: os7.homedir(),
5501
+ uid: process.getuid ? String(process.getuid()) : void 0,
5502
+ windowsUser: platform2 === "win32" ? windowsPrincipalUser() : void 0,
5503
+ start
5504
+ });
5505
+ try {
5506
+ runInstallPlan(plan);
5507
+ } catch (err) {
5508
+ console.error(`Install failed: ${err.message}`);
5509
+ process.exitCode = 1;
5510
+ return;
5511
+ }
5512
+ console.log(
5513
+ `Installed Onklave daemon as a ${plan.manager} service (${plan.serviceLabel}), running as ${os7.userInfo().username}.`
5514
+ );
5515
+ console.log(
5516
+ plan.startsNow ? "Started now and set to auto-start at login." : "Registered to auto-start at next login (not started now \u2014 re-run without --no-start to start immediately)."
5517
+ );
5518
+ console.log("Check it with: onklave daemon status");
5519
+ }
5520
+ async function daemonInstallSystem(platform2, args, start) {
5521
+ if (platform2 === "darwin") {
5522
+ console.error(
5523
+ "--system is not supported on macOS. Use the per-user installer instead:"
5524
+ );
5525
+ console.error(" onklave daemon install");
5526
+ process.exitCode = 1;
5527
+ return;
5528
+ }
5529
+ const credsFile = getFlagValue(args, "--creds-file");
5530
+ if (credsFile) {
5531
+ let credentialsJson2;
5532
+ try {
5533
+ credentialsJson2 = fs11.readFileSync(credsFile, "utf8");
5534
+ } catch (err) {
5535
+ console.error(
5536
+ `Cannot read handed-over credentials: ${err.message}`
5537
+ );
5538
+ process.exitCode = 1;
5539
+ return;
5540
+ }
5541
+ return runSystemInstall(platform2, credentialsJson2, start);
5542
+ }
5543
+ const creds = await new AuthService().getCredentials();
5544
+ if (!creds?.deviceToken || !creds?.machineId) {
5545
+ console.error(
5546
+ "Error: machine is not registered. Run: onklave register --token <bootstrap>"
5547
+ );
5548
+ process.exitCode = 1;
5549
+ return;
5550
+ }
5551
+ const credentialsJson = JSON.stringify(creds);
5552
+ if (platform2 === "win32") {
5553
+ if (isElevated("win32")) {
5554
+ return runSystemInstall("win32", credentialsJson, start);
5555
+ }
5556
+ console.log("Requesting elevation (a UAC prompt will appear)\u2026");
5557
+ const reArgs = [
5558
+ "daemon",
5559
+ "install",
5560
+ "--system",
5561
+ ...start ? [] : ["--no-start"]
5562
+ ];
5563
+ try {
5564
+ runElevated(
5565
+ buildRunAsCommand(process.execPath, [resolveCliPath(), ...reArgs])
5566
+ );
5567
+ } catch (err) {
5568
+ console.error(`Elevation failed: ${err.message}`);
5569
+ process.exitCode = 1;
5570
+ return;
5571
+ }
5572
+ console.log(
5573
+ "Elevated install completed. Verify with: onklave daemon status"
5574
+ );
5575
+ return;
5576
+ }
5577
+ if (isElevated("linux")) {
5578
+ return runSystemInstall("linux", credentialsJson, start);
5579
+ }
5580
+ const tmpDir = fs11.mkdtempSync(path12.join(os7.tmpdir(), "onklave-creds-"));
5581
+ const tmpCreds = path12.join(tmpDir, "credentials.json");
5582
+ fs11.writeFileSync(tmpCreds, credentialsJson, { mode: 384 });
5583
+ try {
5584
+ const reArgs = [
5585
+ "daemon",
5586
+ "install",
5587
+ "--system",
5588
+ ...start ? [] : ["--no-start"],
5589
+ "--creds-file",
5590
+ tmpCreds
5591
+ ];
5592
+ console.log("Requesting elevation via sudo\u2026");
5593
+ runElevated(
5594
+ buildSudoReExec(process.execPath, [resolveCliPath(), ...reArgs])
5595
+ );
5596
+ } catch (err) {
5597
+ console.error(`Elevation failed: ${err.message}`);
5598
+ process.exitCode = 1;
5599
+ } finally {
5600
+ try {
5601
+ fs11.rmSync(tmpDir, { recursive: true, force: true });
5602
+ } catch {
5603
+ }
5604
+ }
5605
+ }
5606
+ async function runSystemInstall(platform2, credentialsJson, start) {
5607
+ if (platform2 === "win32" && !commandExists("nssm")) {
5608
+ console.error(
5609
+ "NSSM is required to supervise a Node process as a Windows service but was not found on PATH."
5610
+ );
5611
+ console.error("Install it (e.g. `choco install nssm`) and re-run.");
5612
+ process.exitCode = 1;
5613
+ return;
5614
+ }
5615
+ if (platform2 === "linux" && !commandExists("useradd")) {
5616
+ console.error(
5617
+ "`useradd` was not found \u2014 cannot create the dedicated service account."
5618
+ );
5619
+ process.exitCode = 1;
5620
+ return;
5621
+ }
5622
+ const configDir = systemConfigDir(platform2);
5623
+ let plan;
5624
+ try {
5625
+ plan = buildInstallPlan({
5626
+ platform: platform2,
5627
+ nodePath: process.execPath,
5628
+ cliPath: resolveCliPath(),
5629
+ homeDir: os7.homedir(),
5630
+ start,
5631
+ system: true,
5632
+ systemConfigDir: configDir,
5633
+ credentialsJson
5634
+ });
5635
+ runInstallPlan(plan);
5636
+ } catch (err) {
5637
+ console.error(`Install failed: ${err.message}`);
5638
+ process.exitCode = 1;
5639
+ return;
5640
+ }
5641
+ console.log(
5642
+ `Installed Onklave daemon as a ${plan.manager} service (${plan.serviceLabel}).`
5643
+ );
5644
+ console.log(
5645
+ `Config dir: ${configDir} \u2014 credentials copied there for the service account.`
5646
+ );
5647
+ console.log(
5648
+ plan.startsNow ? "Started now and enabled at boot." : "Enabled at boot (not started now \u2014 re-run without --no-start to start immediately)."
5649
+ );
5650
+ console.log(
5651
+ platform2 === "linux" ? "Logs: journalctl -u onklave-daemon -f" : `Logs: ${path12.join(configDir, "logs")}`
5652
+ );
5653
+ }
5654
+ async function daemonUninstall(args) {
5655
+ const platform2 = currentServicePlatform();
5656
+ if (!platform2) {
5657
+ console.error(`Unsupported platform: ${process.platform}`);
5658
+ process.exitCode = 1;
5659
+ return;
5660
+ }
5661
+ if (args.includes("--system")) {
5662
+ if (platform2 === "darwin") {
5663
+ console.error(
5664
+ "--system is not supported on macOS. Use: onklave daemon uninstall"
5665
+ );
5666
+ process.exitCode = 1;
5667
+ return;
5668
+ }
5669
+ if (!isElevated(platform2)) {
5670
+ const reArgs = ["daemon", "uninstall", "--system"];
5671
+ const cmd = platform2 === "win32" ? buildRunAsCommand(process.execPath, [resolveCliPath(), ...reArgs]) : buildSudoReExec(process.execPath, [resolveCliPath(), ...reArgs]);
5672
+ console.log("Requesting elevation\u2026");
5673
+ try {
5674
+ runElevated(cmd);
5675
+ } catch (err) {
5676
+ console.error(`Elevation failed: ${err.message}`);
5677
+ process.exitCode = 1;
5678
+ }
5679
+ return;
5680
+ }
5681
+ const configDir = systemConfigDir(platform2);
5682
+ const plan2 = buildUninstallPlan({
5683
+ platform: platform2,
5684
+ homeDir: os7.homedir(),
5685
+ system: true,
5686
+ systemConfigDir: configDir
5687
+ });
5688
+ runUninstallPlan(plan2);
5689
+ console.log(
5690
+ `Removed Onklave daemon ${plan2.manager} service (${plan2.serviceLabel}).`
5691
+ );
5692
+ return;
5693
+ }
5694
+ const plan = buildUninstallPlan({
5695
+ platform: platform2,
5696
+ homeDir: os7.homedir(),
5697
+ uid: process.getuid ? String(process.getuid()) : void 0
5698
+ });
5699
+ runUninstallPlan(plan);
5700
+ console.log(
5701
+ `Removed Onklave daemon ${plan.manager} service (${plan.serviceLabel}).`
5702
+ );
5703
+ }
4773
5704
  async function daemonStop() {
4774
5705
  const pidFile = defaultPidFilePath();
4775
5706
  const pid = readPid(pidFile);
@@ -4802,7 +5733,7 @@ function transitionToAction(next) {
4802
5733
  }
4803
5734
  function readPid(pidFile) {
4804
5735
  try {
4805
- const raw = fs10.readFileSync(pidFile, "utf8").trim();
5736
+ const raw = fs11.readFileSync(pidFile, "utf8").trim();
4806
5737
  const parsed = Number.parseInt(raw, 10);
4807
5738
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
4808
5739
  } catch {
@@ -4810,19 +5741,19 @@ function readPid(pidFile) {
4810
5741
  }
4811
5742
  }
4812
5743
  function writePid(pidFile) {
4813
- const dir = path10.dirname(pidFile);
4814
- fs10.mkdirSync(dir, { recursive: true });
5744
+ const dir = path12.dirname(pidFile);
5745
+ fs11.mkdirSync(dir, { recursive: true });
4815
5746
  try {
4816
- if (fs10.statSync(pidFile).isDirectory()) {
4817
- fs10.rmSync(pidFile, { recursive: true, force: true });
5747
+ if (fs11.statSync(pidFile).isDirectory()) {
5748
+ fs11.rmSync(pidFile, { recursive: true, force: true });
4818
5749
  }
4819
5750
  } catch {
4820
5751
  }
4821
- fs10.writeFileSync(pidFile, String(process.pid), { mode: 384 });
5752
+ fs11.writeFileSync(pidFile, String(process.pid), { mode: 384 });
4822
5753
  }
4823
5754
  function removePid(pidFile) {
4824
5755
  try {
4825
- fs10.unlinkSync(pidFile);
5756
+ fs11.unlinkSync(pidFile);
4826
5757
  } catch {
4827
5758
  }
4828
5759
  }
@@ -4866,14 +5797,14 @@ var COMMANDS = {
4866
5797
  };
4867
5798
 
4868
5799
  // _apps/@onklave/agent-cli/src/services/update-notifier.service.ts
4869
- import * as fs11 from "node:fs";
4870
- import * as path11 from "node:path";
5800
+ import * as fs12 from "node:fs";
5801
+ import * as path13 from "node:path";
4871
5802
  import * as os8 from "node:os";
4872
5803
  import { createInterface } from "node:readline/promises";
4873
5804
  import { spawnSync } from "node:child_process";
4874
5805
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
4875
5806
  var FETCH_DEADLINE_MS = 1500;
4876
- var CACHE_PATH = path11.join(
5807
+ var CACHE_PATH = path13.join(
4877
5808
  os8.homedir(),
4878
5809
  ".config",
4879
5810
  "onklave",
@@ -4984,16 +5915,16 @@ function isCacheFresh(cache, nowMs) {
4984
5915
  }
4985
5916
  function readCache() {
4986
5917
  try {
4987
- if (!fs11.existsSync(CACHE_PATH)) return {};
4988
- return JSON.parse(fs11.readFileSync(CACHE_PATH, "utf8"));
5918
+ if (!fs12.existsSync(CACHE_PATH)) return {};
5919
+ return JSON.parse(fs12.readFileSync(CACHE_PATH, "utf8"));
4989
5920
  } catch {
4990
5921
  return {};
4991
5922
  }
4992
5923
  }
4993
5924
  function writeCache(cache) {
4994
5925
  try {
4995
- fs11.mkdirSync(path11.dirname(CACHE_PATH), { recursive: true, mode: 448 });
4996
- fs11.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
5926
+ fs12.mkdirSync(path13.dirname(CACHE_PATH), { recursive: true, mode: 448 });
5927
+ fs12.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4997
5928
  } catch {
4998
5929
  }
4999
5930
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onklave/agent-cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Onklave Agent CLI — local agent runner with cloud orchestration",
5
5
  "bin": {
6
6
  "onklave": "./main.js"