@onklave/agent-cli 0.1.11 → 0.1.12

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 +550 -3
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -604,8 +604,8 @@ var PlatformClient = class {
604
604
  /*
605
605
  * Make an authenticated HTTP request to the platform.
606
606
  */
607
- async request(method, path6, body) {
608
- const url = `${this.baseUrl}${path6}`;
607
+ async request(method, path7, body) {
608
+ const url = `${this.baseUrl}${path7}`;
609
609
  const headers = {
610
610
  Authorization: `Bearer ${this.token}`,
611
611
  "Content-Type": "application/json",
@@ -908,6 +908,20 @@ var SessionManager = class {
908
908
  }
909
909
  };
910
910
 
911
+ // _apps/@onklave/agent-cli/src/types/events.types.ts
912
+ var DAEMON_AUDIT_ACTIONS = {
913
+ STARTED: "daemon.started",
914
+ ONLINE: "daemon.online",
915
+ DRAINING: "daemon.draining",
916
+ STOPPED: "daemon.stopped",
917
+ FORCE_STOPPED: "daemon.force_stopped",
918
+ DRAIN_TIMEOUT_EXCEEDED: "daemon.drain_timeout_exceeded",
919
+ AUTH_WARNING: "daemon.auth_warning",
920
+ AUTH_EXPIRED: "daemon.auth_expired",
921
+ HEARTBEAT_FAILED: "daemon.heartbeat_failed",
922
+ RESOURCE_LIMIT_BREACHED: "daemon.resource_limit_breached"
923
+ };
924
+
911
925
  // _apps/@onklave/agent-cli/src/services/state-publisher.ts
912
926
  var StatePublisher = class {
913
927
  constructor(commsClient) {
@@ -2846,6 +2860,538 @@ async function logsCommand(args) {
2846
2860
  }
2847
2861
  }
2848
2862
 
2863
+ // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
2864
+ import * as fs6 from "fs";
2865
+
2866
+ // _apps/@onklave/agent-cli/src/services/daemon-comms.service.ts
2867
+ var DaemonCommsService = class {
2868
+ constructor(opts, client) {
2869
+ this.disconnectedAt = null;
2870
+ this.stopRequested = false;
2871
+ this.client = client ?? new CommsClient();
2872
+ this.opts = opts;
2873
+ this.sleep = opts.sleep ?? defaultSleep;
2874
+ this.onRetry = opts.onRetry ?? ((attempt, delayMs, err) => console.warn(
2875
+ `[daemon-comms] reconnect attempt ${attempt} after ${delayMs}ms (${err.message})`
2876
+ ));
2877
+ }
2878
+ /**
2879
+ * Opens the socket connection. Retries indefinitely with jittered
2880
+ * exponential backoff until either (a) a connect succeeds or (b)
2881
+ * `stop()` is called. Throws only if `stop()` is invoked before any
2882
+ * successful connect.
2883
+ */
2884
+ async connect() {
2885
+ let attempt = 0;
2886
+ while (!this.stopRequested) {
2887
+ try {
2888
+ await this.client.connect(this.opts.platformUrl, this.opts.token, {
2889
+ machineId: this.opts.machineId,
2890
+ deviceToken: this.opts.deviceToken,
2891
+ orgId: this.opts.orgId
2892
+ });
2893
+ this.disconnectedAt = null;
2894
+ return;
2895
+ } catch (err) {
2896
+ attempt += 1;
2897
+ const delay = this.computeBackoff(attempt);
2898
+ this.onRetry(attempt, delay, err);
2899
+ if (this.disconnectedAt == null) this.disconnectedAt = /* @__PURE__ */ new Date();
2900
+ await this.sleep(delay);
2901
+ }
2902
+ }
2903
+ throw new Error("daemon-comms: stop() called before any connect succeeded");
2904
+ }
2905
+ /**
2906
+ * Signal that no further reconnect should be attempted and close the
2907
+ * underlying socket cleanly. Idempotent.
2908
+ */
2909
+ disconnect() {
2910
+ this.stopRequested = true;
2911
+ this.client.disconnect();
2912
+ }
2913
+ inner() {
2914
+ return this.client;
2915
+ }
2916
+ isConnected() {
2917
+ return this.client.isConnected();
2918
+ }
2919
+ /**
2920
+ * Continuous-disconnect duration in ms (null if currently connected).
2921
+ * Per spec §3.3, when this exceeds 120s the daemon's `status` output
2922
+ * should report `offline` — the platform will have done so via
2923
+ * heartbeat staleness anyway.
2924
+ */
2925
+ disconnectedForMs() {
2926
+ if (!this.disconnectedAt) return null;
2927
+ return Date.now() - this.disconnectedAt.getTime();
2928
+ }
2929
+ computeBackoff(attempt) {
2930
+ const max = this.opts.maxBackoffMs ?? 6e4;
2931
+ const base = Math.min(max, 1e3 * 2 ** (attempt - 1));
2932
+ const j = this.opts.jitter ?? 0.2;
2933
+ const swing = base * j;
2934
+ return Math.round(base + (Math.random() * 2 - 1) * swing);
2935
+ }
2936
+ };
2937
+ function defaultSleep(ms) {
2938
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2939
+ }
2940
+
2941
+ // _apps/@onklave/agent-cli/src/services/daemon-state.service.ts
2942
+ import * as fs5 from "fs";
2943
+ import * as path6 from "path";
2944
+ import * as os4 from "os";
2945
+ var VALID_TRANSITIONS = {
2946
+ installing: ["registered"],
2947
+ registered: ["starting"],
2948
+ starting: ["online", "stopping"],
2949
+ online: ["draining", "stopping"],
2950
+ draining: ["stopping"],
2951
+ stopping: ["stopped"],
2952
+ stopped: ["starting"]
2953
+ };
2954
+ function defaultStateFilePath() {
2955
+ return path6.join(os4.homedir(), ".config", "onklave", "daemon.state.json");
2956
+ }
2957
+ function defaultPidFilePath() {
2958
+ return path6.join(os4.homedir(), ".config", "onklave", "daemon.pid");
2959
+ }
2960
+ var DaemonStateError = class extends Error {
2961
+ constructor(message) {
2962
+ super(message);
2963
+ this.name = "DaemonStateError";
2964
+ }
2965
+ };
2966
+ var DaemonStateService = class {
2967
+ constructor(stateFile = defaultStateFilePath(), initial = "registered", machineId) {
2968
+ this.stateFile = stateFile;
2969
+ this.machineId = machineId;
2970
+ this.listeners = [];
2971
+ this.current = initial;
2972
+ this.enteredAt = /* @__PURE__ */ new Date();
2973
+ }
2974
+ /**
2975
+ * Read the persisted state from disk, if any. Used by `daemon status`
2976
+ * when the daemon process is not running. Returns null on missing or
2977
+ * malformed file rather than throwing so the caller can decide.
2978
+ */
2979
+ static readPersisted(stateFile = defaultStateFilePath()) {
2980
+ try {
2981
+ if (!fs5.existsSync(stateFile)) return null;
2982
+ const raw = fs5.readFileSync(stateFile, "utf8");
2983
+ const parsed = JSON.parse(raw);
2984
+ if (!parsed.state || !parsed.enteredAt) return null;
2985
+ return parsed;
2986
+ } catch {
2987
+ return null;
2988
+ }
2989
+ }
2990
+ getCurrent() {
2991
+ return this.current;
2992
+ }
2993
+ getEnteredAt() {
2994
+ return this.enteredAt;
2995
+ }
2996
+ /**
2997
+ * Subscribe to transitions. Listeners are awaited sequentially in
2998
+ * registration order. If a listener throws, the transition is treated
2999
+ * as failed — the state is rolled back and the error propagates so the
3000
+ * caller can decide. (Audit emission listeners that fail must therefore
3001
+ * fail the transition — that is the constitutional `audit-fails-action`
3002
+ * coupling described in spec §1.4.)
3003
+ */
3004
+ onTransition(cb) {
3005
+ this.listeners.push(cb);
3006
+ }
3007
+ /**
3008
+ * Attempt a transition. Throws DaemonStateError on illegal transition
3009
+ * (current → next not in VALID_TRANSITIONS). Throws whatever a listener
3010
+ * throws if any listener fails; in that case state is rolled back to
3011
+ * `prev` and is not persisted.
3012
+ */
3013
+ async transition(next, opts = {}) {
3014
+ const prev = this.current;
3015
+ const allowed = VALID_TRANSITIONS[prev];
3016
+ if (!allowed.includes(next)) {
3017
+ throw new DaemonStateError(`illegal daemon transition ${prev} \u2192 ${next}`);
3018
+ }
3019
+ this.current = next;
3020
+ this.enteredAt = /* @__PURE__ */ new Date();
3021
+ try {
3022
+ for (const listener of this.listeners) {
3023
+ await listener(next, prev, opts.reason);
3024
+ }
3025
+ this.persist(opts.reason);
3026
+ } catch (err) {
3027
+ this.current = prev;
3028
+ throw err;
3029
+ }
3030
+ }
3031
+ persist(reason) {
3032
+ const dir = path6.dirname(this.stateFile);
3033
+ fs5.mkdirSync(dir, { recursive: true });
3034
+ const payload = {
3035
+ state: this.current,
3036
+ enteredAt: this.enteredAt.toISOString(),
3037
+ pid: process.pid,
3038
+ ...this.machineId ? { machineId: this.machineId } : {},
3039
+ ...reason ? { reason } : {}
3040
+ };
3041
+ const tmp = `${this.stateFile}.tmp`;
3042
+ fs5.writeFileSync(tmp, JSON.stringify(payload, null, 2));
3043
+ fs5.renameSync(tmp, this.stateFile);
3044
+ }
3045
+ /**
3046
+ * Remove the persisted state file. Called on terminal `stopped` once
3047
+ * the daemon process is exiting cleanly so `daemon status` reports
3048
+ * "registered — daemon not running" instead of stale "stopped".
3049
+ */
3050
+ clearPersisted() {
3051
+ try {
3052
+ fs5.unlinkSync(this.stateFile);
3053
+ } catch {
3054
+ }
3055
+ }
3056
+ };
3057
+
3058
+ // _apps/@onklave/agent-cli/src/services/unit-file-emitters.ts
3059
+ var SYSTEMD_UNIT = `[Unit]
3060
+ Description=Onklave Agent CLI Daemon
3061
+ Documentation=https://docs.onklave.app/cli/daemon
3062
+ After=network-online.target
3063
+ Wants=network-online.target
3064
+
3065
+ [Service]
3066
+ Type=simple
3067
+ User=onklave
3068
+ Group=onklave
3069
+ Environment="NODE_ENV=production"
3070
+ Environment="ONKLAVE_CONFIG_DIR=/var/lib/onklave"
3071
+ Environment="ONKLAVE_LOG_DIR=/var/log/onklave"
3072
+ ExecStart=/usr/local/bin/onklave daemon
3073
+ ExecStop=/usr/local/bin/onklave daemon drain --timeout 1800
3074
+ Restart=on-failure
3075
+ RestartSec=10s
3076
+ StartLimitIntervalSec=300
3077
+ StartLimitBurst=5
3078
+ KillMode=mixed
3079
+ KillSignal=SIGTERM
3080
+ TimeoutStopSec=1830s
3081
+
3082
+ # Hardening
3083
+ NoNewPrivileges=true
3084
+ ProtectSystem=strict
3085
+ ProtectHome=true
3086
+ PrivateTmp=true
3087
+ ReadWritePaths=/var/lib/onklave /var/log/onklave
3088
+
3089
+ [Install]
3090
+ WantedBy=multi-user.target
3091
+ `;
3092
+ var LAUNCHD_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
3093
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3094
+ <plist version="1.0">
3095
+ <dict>
3096
+ <key>Label</key>
3097
+ <string>app.onklave.daemon</string>
3098
+ <key>ProgramArguments</key>
3099
+ <array>
3100
+ <string>/usr/local/bin/onklave</string>
3101
+ <string>daemon</string>
3102
+ </array>
3103
+ <key>RunAtLoad</key>
3104
+ <true/>
3105
+ <key>KeepAlive</key>
3106
+ <dict>
3107
+ <key>SuccessfulExit</key>
3108
+ <false/>
3109
+ <key>Crashed</key>
3110
+ <true/>
3111
+ </dict>
3112
+ <key>ThrottleInterval</key>
3113
+ <integer>10</integer>
3114
+ <key>EnvironmentVariables</key>
3115
+ <dict>
3116
+ <key>NODE_ENV</key>
3117
+ <string>production</string>
3118
+ <key>ONKLAVE_CONFIG_DIR</key>
3119
+ <string>/Users/Shared/onklave</string>
3120
+ </dict>
3121
+ <key>StandardOutPath</key>
3122
+ <string>/Users/Shared/onklave/logs/daemon.out.log</string>
3123
+ <key>StandardErrorPath</key>
3124
+ <string>/Users/Shared/onklave/logs/daemon.err.log</string>
3125
+ <key>ProcessType</key>
3126
+ <string>Background</string>
3127
+ </dict>
3128
+ </plist>
3129
+ `;
3130
+ var NSSM_SCRIPT = `@echo off
3131
+ REM Onklave Agent CLI \u2014 NSSM service install script
3132
+ REM Run this as Administrator.
3133
+
3134
+ set NSSM=nssm.exe
3135
+ set SERVICE=OnklaveDaemon
3136
+ set NODE_EXE=%ProgramFiles%\\nodejs\\node.exe
3137
+ set ONKLAVE_CLI=%AppData%\\npm\\node_modules\\@onklave\\agent-cli\\dist\\bin\\onklave.js
3138
+
3139
+ %NSSM% install %SERVICE% "%NODE_EXE%" "%ONKLAVE_CLI%" daemon
3140
+ %NSSM% set %SERVICE% DisplayName "Onklave Agent CLI Daemon"
3141
+ %NSSM% set %SERVICE% Description "Long-running agent worker for the Onklave platform"
3142
+ %NSSM% set %SERVICE% Start SERVICE_AUTO_START
3143
+ %NSSM% set %SERVICE% AppEnvironmentExtra NODE_ENV=production ONKLAVE_CONFIG_DIR=%ProgramData%\\Onklave
3144
+ %NSSM% set %SERVICE% AppStdout %ProgramData%\\Onklave\\logs\\daemon.out.log
3145
+ %NSSM% set %SERVICE% AppStderr %ProgramData%\\Onklave\\logs\\daemon.err.log
3146
+ %NSSM% set %SERVICE% AppRotateFiles 1
3147
+ %NSSM% set %SERVICE% AppRotateOnline 1
3148
+ %NSSM% set %SERVICE% AppRotateBytes 10485760
3149
+ %NSSM% set %SERVICE% AppExit Default Restart
3150
+ %NSSM% set %SERVICE% AppRestartDelay 10000
3151
+
3152
+ %NSSM% start %SERVICE%
3153
+ `;
3154
+ function emitUnitFile(flavour) {
3155
+ switch (flavour) {
3156
+ case "systemd":
3157
+ return SYSTEMD_UNIT;
3158
+ case "launchd":
3159
+ return LAUNCHD_PLIST;
3160
+ case "nssm":
3161
+ return NSSM_SCRIPT;
3162
+ }
3163
+ }
3164
+
3165
+ // _apps/@onklave/agent-cli/src/commands/daemon.command.ts
3166
+ var UNIT_FLAGS = {
3167
+ "--emit-systemd-unit": "systemd",
3168
+ "--emit-launchd-plist": "launchd",
3169
+ "--emit-nssm-script": "nssm"
3170
+ };
3171
+ async function daemonCommand(args) {
3172
+ for (const arg of args) {
3173
+ const flavour = UNIT_FLAGS[arg];
3174
+ if (flavour) {
3175
+ process.stdout.write(emitUnitFile(flavour));
3176
+ return;
3177
+ }
3178
+ }
3179
+ const [sub] = args;
3180
+ if (sub === "status") return daemonStatus();
3181
+ if (sub === "stop" || sub === "drain") return daemonStop();
3182
+ if (sub && !sub.startsWith("--")) {
3183
+ console.error(`Unknown subcommand: ${sub}`);
3184
+ console.error(
3185
+ "Usage: onklave daemon [status|stop|drain] | --emit-systemd-unit | --emit-launchd-plist | --emit-nssm-script"
3186
+ );
3187
+ process.exitCode = 1;
3188
+ return;
3189
+ }
3190
+ return daemonStart();
3191
+ }
3192
+ async function daemonStart() {
3193
+ const authService = new AuthService();
3194
+ const creds = await authService.getCredentials();
3195
+ if (!creds?.deviceToken || !creds?.machineId) {
3196
+ console.error(
3197
+ "Error: machine is not registered. Run: onklave register --token <bootstrap>"
3198
+ );
3199
+ process.exitCode = 1;
3200
+ return;
3201
+ }
3202
+ const pidFile = defaultPidFilePath();
3203
+ if (isPidAlive(pidFile)) {
3204
+ console.error(`Daemon already running (pid ${readPid(pidFile)}).`);
3205
+ process.exitCode = 1;
3206
+ return;
3207
+ }
3208
+ const stateService = new DaemonStateService(
3209
+ defaultStateFilePath(),
3210
+ "registered",
3211
+ creds.machineId
3212
+ );
3213
+ const platformUrl = await authService.getPlatformUrl();
3214
+ const comms = new DaemonCommsService({
3215
+ platformUrl,
3216
+ token: creds.token,
3217
+ machineId: creds.machineId,
3218
+ deviceToken: creds.deviceToken,
3219
+ orgId: creds.orgId
3220
+ });
3221
+ const auditStreamer = new AuditStreamer(comms.inner());
3222
+ stateService.onTransition(async (next, prev, reason) => {
3223
+ const action = transitionToAction(next);
3224
+ if (!action) return;
3225
+ const event = {
3226
+ sessionId: creds.machineId,
3227
+ type: "daemon_lifecycle" /* DAEMON_LIFECYCLE */,
3228
+ action,
3229
+ details: { from: prev, to: next, ...reason ? { reason } : {} },
3230
+ outcome: next === "stopped" && reason ? "failure" : "success"
3231
+ };
3232
+ auditStreamer.record(event);
3233
+ });
3234
+ let activeSessions = 0;
3235
+ let shuttingDown = false;
3236
+ let sigintCount = 0;
3237
+ let sigintTimer = null;
3238
+ const heartbeat = new HeartbeatService({
3239
+ platformUrl,
3240
+ deviceToken: creds.deviceToken,
3241
+ machineId: creds.machineId,
3242
+ getActiveSessionCount: () => activeSessions
3243
+ });
3244
+ const drainAndExit = async (reason) => {
3245
+ if (shuttingDown) return;
3246
+ shuttingDown = true;
3247
+ heartbeat.stop();
3248
+ try {
3249
+ if (stateService.getCurrent() === "online") {
3250
+ await stateService.transition("draining", { reason });
3251
+ }
3252
+ while (activeSessions > 0) {
3253
+ await new Promise((r) => setTimeout(r, 200));
3254
+ }
3255
+ await stateService.transition("stopping", { reason });
3256
+ auditStreamer.flush();
3257
+ comms.disconnect();
3258
+ await stateService.transition("stopped");
3259
+ } finally {
3260
+ removePid(pidFile);
3261
+ stateService.clearPersisted();
3262
+ process.exit(0);
3263
+ }
3264
+ };
3265
+ process.on("SIGTERM", () => {
3266
+ void drainAndExit("SIGTERM");
3267
+ });
3268
+ process.on("SIGINT", () => {
3269
+ sigintCount += 1;
3270
+ if (sigintCount >= 2) {
3271
+ console.error("\nForce-stop requested. Exiting immediately.");
3272
+ removePid(pidFile);
3273
+ process.exit(1);
3274
+ }
3275
+ if (!sigintTimer) {
3276
+ sigintTimer = setTimeout(() => {
3277
+ sigintCount = 0;
3278
+ sigintTimer = null;
3279
+ }, 5e3);
3280
+ sigintTimer.unref?.();
3281
+ }
3282
+ console.log(
3283
+ "\nSIGINT received. Draining (Ctrl-C again within 5s to force stop)\u2026"
3284
+ );
3285
+ void drainAndExit("SIGINT");
3286
+ });
3287
+ await stateService.transition("starting");
3288
+ writePid(pidFile);
3289
+ console.log("Connecting to Onklave comms service...");
3290
+ try {
3291
+ await comms.connect();
3292
+ } catch (err) {
3293
+ console.error(
3294
+ `Comms connect aborted: ${err.message}. Shutting down.`
3295
+ );
3296
+ removePid(pidFile);
3297
+ stateService.clearPersisted();
3298
+ process.exit(1);
3299
+ }
3300
+ console.log("Connected.");
3301
+ await stateService.transition("online");
3302
+ heartbeat.start();
3303
+ console.log(
3304
+ `Daemon online. machineId=${creds.machineId} pid=${process.pid}. Press Ctrl-C to drain.`
3305
+ );
3306
+ await new Promise(() => void 0);
3307
+ }
3308
+ async function daemonStatus() {
3309
+ const persisted = DaemonStateService.readPersisted();
3310
+ if (!persisted) {
3311
+ console.log("registered \u2014 daemon not running");
3312
+ return;
3313
+ }
3314
+ const alive = persisted.pid ? isProcessAlive(persisted.pid) : false;
3315
+ if (!alive) {
3316
+ console.log(
3317
+ `${persisted.state} \u2014 daemon process not running (last entered ${persisted.enteredAt})`
3318
+ );
3319
+ return;
3320
+ }
3321
+ console.log(`State: ${persisted.state}`);
3322
+ if (persisted.machineId)
3323
+ console.log(`Machine ID: ${persisted.machineId}`);
3324
+ if (persisted.pid) console.log(`PID: ${persisted.pid}`);
3325
+ console.log(`Entered state: ${persisted.enteredAt}`);
3326
+ if (persisted.reason) console.log(`Reason: ${persisted.reason}`);
3327
+ }
3328
+ async function daemonStop() {
3329
+ const pidFile = defaultPidFilePath();
3330
+ const pid = readPid(pidFile);
3331
+ if (!pid || !isProcessAlive(pid)) {
3332
+ console.log("No daemon running.");
3333
+ removePid(pidFile);
3334
+ return;
3335
+ }
3336
+ try {
3337
+ process.kill(pid, "SIGTERM");
3338
+ console.log(`Sent SIGTERM to daemon (pid ${pid}).`);
3339
+ } catch (err) {
3340
+ console.error(`Failed to signal daemon: ${err.message}`);
3341
+ process.exitCode = 1;
3342
+ }
3343
+ }
3344
+ function transitionToAction(next) {
3345
+ switch (next) {
3346
+ case "starting":
3347
+ return DAEMON_AUDIT_ACTIONS.STARTED;
3348
+ case "online":
3349
+ return DAEMON_AUDIT_ACTIONS.ONLINE;
3350
+ case "draining":
3351
+ return DAEMON_AUDIT_ACTIONS.DRAINING;
3352
+ case "stopped":
3353
+ return DAEMON_AUDIT_ACTIONS.STOPPED;
3354
+ default:
3355
+ return null;
3356
+ }
3357
+ }
3358
+ function readPid(pidFile) {
3359
+ try {
3360
+ const raw = fs6.readFileSync(pidFile, "utf8").trim();
3361
+ const parsed = Number.parseInt(raw, 10);
3362
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
3363
+ } catch {
3364
+ return null;
3365
+ }
3366
+ }
3367
+ function writePid(pidFile) {
3368
+ const dir = pidFile.replace(/\/[^/]+$/, "");
3369
+ fs6.mkdirSync(dir, { recursive: true });
3370
+ fs6.writeFileSync(pidFile, String(process.pid), { mode: 384 });
3371
+ }
3372
+ function removePid(pidFile) {
3373
+ try {
3374
+ fs6.unlinkSync(pidFile);
3375
+ } catch {
3376
+ }
3377
+ }
3378
+ function isPidAlive(pidFile) {
3379
+ const pid = readPid(pidFile);
3380
+ if (pid == null) return false;
3381
+ return isProcessAlive(pid);
3382
+ }
3383
+ function isProcessAlive(pid) {
3384
+ try {
3385
+ process.kill(pid, 0);
3386
+ return true;
3387
+ } catch (err) {
3388
+ if (err.code === "EPERM") {
3389
+ return true;
3390
+ }
3391
+ return false;
3392
+ }
3393
+ }
3394
+
2849
3395
  // _apps/@onklave/agent-cli/src/commands/index.ts
2850
3396
  var COMMANDS = {
2851
3397
  login: loginCommand,
@@ -2864,7 +3410,8 @@ var COMMANDS = {
2864
3410
  init: (_args) => initCommand(),
2865
3411
  config: configCommand,
2866
3412
  register: registerCommand,
2867
- logs: logsCommand
3413
+ logs: logsCommand,
3414
+ daemon: daemonCommand
2868
3415
  };
2869
3416
 
2870
3417
  // _apps/@onklave/agent-cli/src/main.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onklave/agent-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Onklave Agent CLI — local agent runner with cloud orchestration",
5
5
  "bin": {
6
6
  "onklave": "./main.js"