@ours.network/cli 0.5.0 → 1.0.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 CHANGED
@@ -1,9 +1,10 @@
1
1
  # @ours.network/cli
2
2
 
3
- `ours` is the transport-neutral operator CLI for the shared ours daemon. It is a
4
- separate package above `@ours.network/sdk`: lifecycle commands call
5
- `startDaemon`, and network operations use `OursClient`. It has no dependency on
6
- `@ours.network/mcp`.
3
+ `ours` is the transport-neutral operator CLI and lifecycle owner for the shared
4
+ ours daemon. Its artifact bundles the SDK's package-private daemon runtime;
5
+ network operations use the public client attach path. It has no dependency on
6
+ `@ours.network/mcp`, and applications cannot import the runtime through an SDK
7
+ daemon subpath.
7
8
 
8
9
  ## Install and first checks
9
10
 
@@ -91,12 +92,12 @@ and secret fields already in the file.
91
92
  ```bash
92
93
  ours config setup --port 3070 --state-dir /srv/ours-a \
93
94
  --broker-url wss://broker1.ours.network --api-visibility owner
94
- ours config setup --auto-start false --gc-interval-ms 3600000
95
+ ours config setup --gc-interval-ms 3600000
95
96
  ours config setup --port 3070 --dry-run --json
96
97
  ```
97
98
 
98
- Supported setup fields are `brokerUrl`, `port`, `stateDir`, `gcIntervalMs`,
99
- `autoStart`, and `apiVisibility`. Supply credentials through an operator-owned
99
+ Supported setup fields are `brokerUrl`, `port`, `stateDir`, `gcIntervalMs`, and
100
+ `apiVisibility`. Supply credentials through an operator-owned
100
101
  secret mechanism or the existing SDK configuration, not CLI arguments.
101
102
 
102
103
  ## Operator commands
@@ -151,7 +152,7 @@ file fallback probes, and legacy monitoring/control-plane operations.
151
152
 
152
153
  ## Service management
153
154
 
154
- Linux user systemd is supported through a testable adapter:
155
+ Linux user systemd and macOS launchd are supported through tested adapters:
155
156
 
156
157
  ```bash
157
158
  ours daemon install-service --dry-run --json
@@ -159,11 +160,10 @@ ours daemon install-service --yes
159
160
  ours daemon uninstall-service --yes
160
161
  ```
161
162
 
162
- The adapter owns only `~/.config/systemd/user/ours.service` files carrying its
163
- management marker. It refuses to overwrite or remove an unrelated unit. Other
164
- platforms fail explicitly; use the platform's external service manager to run
165
- `ours daemon serve`. Service installation is not emulated and never reports
166
- success on an unsupported platform.
163
+ The adapters own only units/plists carrying their management marker and refuse
164
+ to overwrite or remove unrelated definitions. The daemon's state-root lock is
165
+ supported on Linux and macOS only; other platforms refuse startup explicitly
166
+ rather than silently running without singleton protection.
167
167
 
168
168
  ## License
169
169
 
@@ -0,0 +1,9 @@
1
+ import {
2
+ bootWrapper,
3
+ wrapperBooted
4
+ } from "./chunk-MHVYJY3H.js";
5
+ import "./chunk-FXKFSNKS.js";
6
+ export {
7
+ bootWrapper,
8
+ wrapperBooted
9
+ };
@@ -4,8 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import {
7
- OursClient,
8
- assertDaemonStateDir,
7
+ attachOursClient,
9
8
  describeDaemonConfig,
10
9
  resolveDaemonConfig
11
10
  } from "@ours.network/sdk/client";
@@ -26,11 +25,11 @@ function redactedSelection(config) {
26
25
  return describeDaemonConfig(config);
27
26
  }
28
27
  async function attachClient(config, opts = {}) {
29
- await assertDaemonStateDir(config, { fetch: opts.fetch });
30
- return new OursClient({
31
- url: config.baseUrl.value,
28
+ return attachOursClient({
29
+ endpoint: config.baseUrl.value,
30
+ expectStateDir: config.expectStateDir,
31
+ token: config.token?.value,
32
32
  leaseToken: opts.leaseToken ?? randomBytes(32).toString("hex"),
33
- apiToken: config.token?.value,
34
33
  fetch: opts.fetch
35
34
  });
36
35
  }
@@ -2,12 +2,12 @@ import {
2
2
  defaultConfigPath,
3
3
  redactedSelection,
4
4
  resolveSelection
5
- } from "./chunk-6W3RHW3C.js";
5
+ } from "./chunk-6K2JBKHI.js";
6
6
 
7
7
  // src/config-command.ts
8
8
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
9
  import { dirname, resolve } from "node:path";
10
- var SAFE_KEYS = /* @__PURE__ */ new Set(["brokerUrl", "port", "stateDir", "gcIntervalMs", "autoStart", "apiVisibility"]);
10
+ var SAFE_KEYS = /* @__PURE__ */ new Set(["brokerUrl", "port", "stateDir", "gcIntervalMs", "apiVisibility"]);
11
11
  async function showConfig(selection) {
12
12
  const resolved = await resolveSelection(selection);
13
13
  let stored = {};
@@ -0,0 +1,166 @@
1
+ // ../../src/config.ts
2
+ import * as fs from "node:fs";
3
+ import { randomBytes } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { resolve, join, dirname, basename } from "node:path";
6
+ var API_VISIBILITIES = ["owner", "shared", "open"];
7
+ var DEFAULT_CONFIG = {
8
+ brokerUrl: "wss://broker1.ours.network",
9
+ port: 3050,
10
+ stateDir: resolve(homedir(), ".ours"),
11
+ gcIntervalMs: 36e5,
12
+ apiVisibility: "owner"
13
+ };
14
+ function configPath() {
15
+ return process.env.OURS_CONFIG ?? join(homedir(), ".ours", "config.json");
16
+ }
17
+ function readFileConfig() {
18
+ let raw;
19
+ try {
20
+ raw = fs.readFileSync(configPath(), "utf8");
21
+ } catch {
22
+ return {};
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ } catch {
28
+ return {};
29
+ }
30
+ const out = {};
31
+ if (typeof parsed.brokerUrl === "string") out.brokerUrl = parsed.brokerUrl;
32
+ if (typeof parsed.port === "number" && Number.isFinite(parsed.port)) out.port = parsed.port;
33
+ if (typeof parsed.stateDir === "string") out.stateDir = resolve(parsed.stateDir);
34
+ if (typeof parsed.gcIntervalMs === "number" && Number.isFinite(parsed.gcIntervalMs)) {
35
+ out.gcIntervalMs = parsed.gcIntervalMs;
36
+ }
37
+ if (typeof parsed.apiVisibility === "string" && API_VISIBILITIES.includes(parsed.apiVisibility)) {
38
+ out.apiVisibility = parsed.apiVisibility;
39
+ }
40
+ if (typeof parsed.apiToken === "string" && parsed.apiToken.trim()) out.apiToken = parsed.apiToken.trim();
41
+ if (parsed.stt && typeof parsed.stt === "object") {
42
+ const s = parsed.stt;
43
+ const stt = {};
44
+ if (typeof s.provider === "string") stt.provider = s.provider;
45
+ if (typeof s.apiKey === "string") stt.apiKey = s.apiKey;
46
+ if (typeof s.model === "string") stt.model = s.model;
47
+ if (typeof s.baseUrl === "string") stt.baseUrl = s.baseUrl;
48
+ if (typeof s.language === "string") stt.language = s.language;
49
+ if (typeof s.maxBytes === "number" && Number.isFinite(s.maxBytes)) stt.maxBytes = s.maxBytes;
50
+ if (typeof s.timeoutMs === "number" && Number.isFinite(s.timeoutMs)) stt.timeoutMs = s.timeoutMs;
51
+ if (s.custom && typeof s.custom === "object") stt.custom = s.custom;
52
+ out.stt = stt;
53
+ }
54
+ return out;
55
+ }
56
+ function envVisibility() {
57
+ const v = process.env.OURS_API_VISIBILITY?.trim().toLowerCase();
58
+ return v && API_VISIBILITIES.includes(v) ? v : void 0;
59
+ }
60
+ function envInt(name) {
61
+ const v = process.env[name];
62
+ if (v === void 0) return void 0;
63
+ const n = parseInt(v, 10);
64
+ return Number.isNaN(n) ? void 0 : n;
65
+ }
66
+ function loadConfig() {
67
+ const file = readFileConfig();
68
+ return {
69
+ brokerUrl: process.env.OURS_BROKER_URL ?? file.brokerUrl ?? DEFAULT_CONFIG.brokerUrl,
70
+ port: envInt("OURS_PORT") ?? file.port ?? DEFAULT_CONFIG.port,
71
+ stateDir: resolve(process.env.OURS_STATE_DIR ?? file.stateDir ?? DEFAULT_CONFIG.stateDir),
72
+ gcIntervalMs: envInt("OURS_GC_INTERVAL_MS") ?? file.gcIntervalMs ?? DEFAULT_CONFIG.gcIntervalMs,
73
+ apiVisibility: envVisibility() ?? file.apiVisibility ?? DEFAULT_CONFIG.apiVisibility,
74
+ apiToken: process.env.OURS_API_TOKEN?.trim() || file.apiToken,
75
+ stt: sttFromEnv(file.stt)
76
+ };
77
+ }
78
+ function sttFromEnv(file) {
79
+ const env = {};
80
+ if (process.env.OURS_STT_PROVIDER?.trim()) env.provider = process.env.OURS_STT_PROVIDER.trim();
81
+ if (process.env.OURS_STT_API_KEY?.trim()) env.apiKey = process.env.OURS_STT_API_KEY.trim();
82
+ if (process.env.OURS_STT_MODEL?.trim()) env.model = process.env.OURS_STT_MODEL.trim();
83
+ if (process.env.OURS_STT_BASE_URL?.trim()) env.baseUrl = process.env.OURS_STT_BASE_URL.trim();
84
+ if (process.env.OURS_STT_LANGUAGE?.trim()) env.language = process.env.OURS_STT_LANGUAGE.trim();
85
+ const merged = { ...file ?? {}, ...env };
86
+ return Object.keys(merged).length ? merged : void 0;
87
+ }
88
+ var API_TOKEN_FILENAME = "daemon-token";
89
+ function apiTokenPath(cfg) {
90
+ return join(cfg.stateDir, API_TOKEN_FILENAME);
91
+ }
92
+ function explicitApiToken(cfg) {
93
+ const env = process.env.OURS_API_TOKEN?.trim();
94
+ if (env) return env;
95
+ const c = cfg.apiToken?.trim();
96
+ return c || void 0;
97
+ }
98
+ function resolveApiToken(cfg, opts = {}) {
99
+ const env = process.env.OURS_API_TOKEN?.trim();
100
+ if (env) return { token: env, source: "env" };
101
+ const c = cfg.apiToken?.trim();
102
+ if (c) return { token: c, source: "config" };
103
+ const path = apiTokenPath(cfg);
104
+ try {
105
+ const fromFile = fs.readFileSync(path, "utf8").trim();
106
+ if (fromFile) return { token: fromFile, source: "file" };
107
+ } catch {
108
+ }
109
+ if (!opts.generate) return null;
110
+ const token = randomBytes(32).toString("hex");
111
+ try {
112
+ fs.mkdirSync(dirname(path), { recursive: true });
113
+ fs.writeFileSync(path, token + "\n", { mode: 384 });
114
+ fs.chmodSync(path, 384);
115
+ } catch {
116
+ }
117
+ return { token, source: "generated" };
118
+ }
119
+ var IDENTITY_FILENAME = ".ours-identity";
120
+ function buildIdentityFile(opts) {
121
+ if (!opts.name.trim()) throw new Error("identity name must not be empty");
122
+ const obj = { identity: opts.name.trim() };
123
+ if (opts.force) obj.force = true;
124
+ obj.expose_local = opts.exposeLocal ?? true;
125
+ obj.local_auto_accept = opts.localAutoAccept ?? true;
126
+ return obj;
127
+ }
128
+ function resolveIdentityFilePath(target) {
129
+ const abs = resolve(target);
130
+ return basename(abs) === IDENTITY_FILENAME ? abs : join(abs, IDENTITY_FILENAME);
131
+ }
132
+ function writeIdentityFile(target, opts, overwrite = false) {
133
+ const obj = buildIdentityFile(opts);
134
+ const path = resolveIdentityFilePath(target);
135
+ if (!overwrite && fs.existsSync(path)) {
136
+ throw new Error(`${path} already exists \u2014 pass overwrite to replace it`);
137
+ }
138
+ fs.mkdirSync(dirname(path), { recursive: true });
139
+ fs.writeFileSync(path, JSON.stringify(obj, null, 2) + "\n");
140
+ return path;
141
+ }
142
+
143
+ // ../../src/runtime/config-drift.ts
144
+ var FROZEN_ENV = ["OURS_STATE_DIR", "OURS_BROKER_URL", "OURS_PORT", "OURS_API_VISIBILITY", "OURS_CONFIG"];
145
+ var envAtLoad = Object.fromEntries(
146
+ FROZEN_ENV.map((k) => [k, process.env[k]])
147
+ );
148
+ var { stateDir: loadedStateDir, brokerUrl: loadedBrokerUrl } = loadConfig();
149
+ function assertConfigNotDrifted() {
150
+ const drifted = FROZEN_ENV.filter((k) => process.env[k] !== envAtLoad[k]);
151
+ if (drifted.length === 0) return;
152
+ const detail = drifted.map((k) => `${k}: was ${JSON.stringify(envAtLoad[k] ?? "(unset)")} at import, is now ${JSON.stringify(process.env[k] ?? "(unset)")}`).join("; ");
153
+ throw new Error(
154
+ `configuration changed after @ours.network/sdk was imported (${detail}). The SDK reads its config ONCE, at module load, because CONFIG and STATE_DIR back process-wide singletons that cannot be re-pointed once identities exist. Setting these after the import does not fail and does not take effect \u2014 without this check the daemon would boot against ${JSON.stringify(loadedStateDir)} and broker ${JSON.stringify(loadedBrokerUrl)} and look like it worked. Set every OURS_* config variable BEFORE the first import of this package.`
155
+ );
156
+ }
157
+
158
+ export {
159
+ loadConfig,
160
+ apiTokenPath,
161
+ explicitApiToken,
162
+ resolveApiToken,
163
+ buildIdentityFile,
164
+ writeIdentityFile,
165
+ assertConfigNotDrifted
166
+ };