@ours.network/codex 0.17.0 → 0.18.0-nightly.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ours",
3
- "version": "0.17.0",
3
+ "version": "0.18.0-nightly.1",
4
4
  "description": "Secure agent-to-agent messaging and explicitly armed live mail wake for Codex CLI.",
5
5
  "author": {
6
6
  "name": "Adapt Toolkit",
package/bin/proxy.mjs CHANGED
@@ -28,4 +28,3 @@ const child = cliPath
28
28
  : spawn('ours-mcp', ['proxy', ...process.argv.slice(2)], { stdio: 'inherit', env });
29
29
  child.on('error', (error) => { process.stderr.write(`ours: cannot launch @ours.network/mcp proxy: ${error.message}\n`); process.exit(1); });
30
30
  child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 0); });
31
-
@@ -21268,6 +21268,7 @@ async function runMonitorMcp() {
21268
21268
  }
21269
21269
  export {
21270
21270
  createMonitorMcpServer,
21271
+ foregroundWatchProcess,
21271
21272
  handleMonitorCommand,
21272
21273
  monitorToolNames,
21273
21274
  runMonitorMcp,
package/install.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Install the native ours.network plugin into the OpenAI Codex CLI:
3
- # 1. ensure the ours daemon (@ours.network/mcp) is installed + running
3
+ # 1. ensure the ours operator CLI and MCP adapter are installed; start the shared daemon
4
4
  # 2. add/upgrade adapt-toolkit/ours-codex-marketplace
5
5
  # 3. install the native `ours` plugin (skills, MCP servers, and hooks)
6
6
  # 4. back up and remove installer-owned legacy config only after verification
@@ -27,18 +27,18 @@ say(){ printf 'ours-install: %s\n' "$1"; }
27
27
  ensure_daemon_latest(){
28
28
  if [ "${OURS_INSTALL_SKIP_DAEMON:-}" = "1" ]; then say "skipping daemon step (OURS_INSTALL_SKIP_DAEMON=1)"; return 0; fi
29
29
  local before after
30
- before="$(ours-mcp --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
31
- say "ensuring @ours.network/mcp@latest…"
32
- npm i -g @ours.network/mcp@latest
33
- after="$(ours-mcp --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
34
- if ! ours-mcp status >/dev/null 2>&1; then
35
- say "starting the ours daemon…"; ours-mcp start || say "could not auto-start; run 'ours-mcp start' if the tools error."
30
+ before="$(ours version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
31
+ say "ensuring @ours.network/cli@latest and @ours.network/mcp@latest…"
32
+ npm i -g @ours.network/cli@latest @ours.network/mcp@latest
33
+ after="$(ours version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
34
+ if ! ours daemon status >/dev/null 2>&1; then
35
+ say "starting the ours daemon…"; ours daemon start || say "could not start; run 'ours daemon start' if the tools error."
36
36
  elif [ -n "$before" ] && [ "$before" != "$after" ]; then
37
- say "daemon upgraded (v${before} → v${after}) — restarting…"; ours-mcp restart || ours-mcp start || true
37
+ say "operator CLI upgraded (v${before} → v${after}) — restarting its daemon…"; ours daemon restart || ours daemon start || true
38
38
  else
39
39
  say "daemon already current (v${after:-unknown})."
40
40
  fi
41
- say "daemon: $(command -v ours-mcp) (v${after:-unknown})"
41
+ say "operator CLI: $(command -v ours) (v${after:-unknown}); MCP adapter: $(command -v ours-mcp)"
42
42
  }
43
43
 
44
44
  # --- 1) daemon (ensure @latest + restart on change) ---
@@ -91,7 +91,7 @@ say "done. The ours MCP server + skill are live for the next Codex session."
91
91
  # --- version echo: show the user they are on latest ---
92
92
  if [ "${OURS_INSTALL_SKIP_DAEMON:-}" != "1" ]; then
93
93
  say "versions:"
94
- say " daemon: $(ours-mcp --version 2>/dev/null | head -1 || echo 'unknown')"
94
+ say " MCP adapter: $(ours-mcp --version 2>/dev/null | head -1 || echo 'unknown')"
95
95
  say " plugin: $(npm ls -g @ours.network/codex 2>/dev/null | grep -oE '@ours\.network/codex@[0-9][0-9.]*' | head -1 || echo '@ours.network/codex (not a global install)')"
96
96
  fi
97
97
  say "next: bind (or create) an identity, then the ours skill tails ours-mcp watch (or polls"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/codex",
3
- "version": "0.17.0",
3
+ "version": "0.18.0-nightly.1",
4
4
  "description": "Native Codex plugin for secure ours.network messaging and explicitly armed, session-scoped live mail wake.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -46,7 +46,8 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@ours.network/mcp": "0.17.0",
49
+ "@ours.network/mcp": "0.18.0-nightly.1",
50
+ "@ours.network/sdk": "2.0.1",
50
51
  "ws": "^8.21.0",
51
52
  "zod": "^3.25.76"
52
53
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ours
3
- description: Use when the user wants to set up or configure ours or ours-fleet, onboard onto the ours network, create or switch an identity, connect with another agent or person, exchange encrypted messages or files, check incoming mail, arm live monitoring, bind a web-messenger control proxy, or spawn/configure/oversee a persistent or temporary fleet agent. Trigger phrases include "set up ours", "set up ours-fleet", "configure fleet", "spawn fleet agent", "use identity X", "send a message", "check my messages", "watch for messages", "wake me on new mail", "bind the monitoring proxy", and "set up the control panel".
3
+ description: Use when the user wants to set up or configure ours or ours-fleet, onboard onto the ours network, create or switch an identity, connect with another agent or person, exchange encrypted messages or files, check incoming mail, arm live monitoring, or spawn/configure/oversee a persistent or temporary fleet agent. Trigger phrases include "set up ours", "set up ours-fleet", "configure fleet", "spawn fleet agent", "use identity X", "send a message", "check my messages", "watch for messages", "wake me on new mail".
4
4
  metadata:
5
5
  codex:
6
6
  tags: [ours, ours.network, a2a, adapt, e2e, messaging, identity]
@@ -23,8 +23,9 @@ are three surfaces:
23
23
 
24
24
  - **Layer 1 — identities** (global): create / bind / switch the identity you act as.
25
25
  - **Layer 2 — messaging** (per the bound identity): invites, contacts, send/read.
26
- - **Control plane** (the host's **Human identity**): bind a human's web-messenger as a
27
- **monitoring & control proxy** that can oversee and command a fleet of agents.
26
+ - **Control plane** (the host's **Human identity**): a human's web-messenger acting as a
27
+ **monitoring & control proxy** over a fleet of agents. **Not available in this release** —
28
+ its MCP tools were removed; see "Control plane" below before offering anything.
28
29
 
29
30
  Identities come in exactly two kinds, in a fixed order:
30
31
 
@@ -78,19 +79,12 @@ allows it for legacy reasons; this skill does not.
78
79
 
79
80
  Walk the user through these, checking each. Stop and help at the first one that isn't done.
80
81
 
81
- 1. **Daemon running.** The MCP tools talk to a local background daemon. Check it:
82
- `ours-mcp status`. If the command is missing, install it: `npm i -g
83
- @ours.network/mcp`, then `ours-mcp start`. For boot-persistence offer
84
- `ours-mcp install-service`. To change broker / port / state dir, run the
85
- interactive `ours-mcp setup` (this edits config only — it is NOT identity setup).
86
- These run on the user's machine; if a step needs them at a terminal, suggest they
87
- type `! ours-mcp status` etc.
88
- Then check optional voice support with `ours-mcp voice-status --json`. If it is
89
- not ready and the user wants voice transcription, ask them to run `ours-install`
90
- in a terminal: it re-detects incomplete setup and reads the provider key with
91
- hidden input. **Never ask for, paste, echo, or put the key in chat/tool arguments.**
92
- Environment-only operators may set `OURS_STT_*` themselves. Troubleshooting and
93
- the exact Telegram OGG/Opus fallback contract are in `references/configuration.md`.
82
+ 1. **Daemon running.** The MCP tools attach to the shared daemon. Check it with
83
+ `ours daemon status`. If the commands are missing, install
84
+ `@ours.network/cli@1.0.1` and `@ours.network/mcp`, then run `ours config setup`
85
+ and `ours daemon start`. For boot persistence offer
86
+ `ours daemon install-service`. These are operator commands; explain the shared
87
+ blast radius and obtain consent before changing configuration or lifecycle.
94
88
  2. **Plugin installed.** Install the native plugin from the ours Codex marketplace, or
95
89
  install `@ours.network/codex` globally and run `ours-codex-install`. Start a new
96
90
  Codex thread after installation. The native package bundles skills, the ours and
@@ -103,8 +97,9 @@ Walk the user through these, checking each. Stop and help at the first one that
103
97
  through `ours-codex`. After an identity is bound, ask whether to arm it. Call
104
98
  `arm_monitor({ identity })` only after an explicit yes. Standard mode remains fully
105
99
  usable for manual `get_messages` checks.
106
- 6. **(Optional) Oversight.** If they want to watch/command a fleet from a phone or
107
- browser, set up the **control-plane monitoring proxy**.
100
+ 6. **Oversight.** If they ask to watch/command a fleet from a phone or browser, say the
101
+ **control-plane monitoring proxy is not available in this release** — there is no tool
102
+ to call. See "Control plane" below.
108
103
 
109
104
  - **Configuration.** Port, state dir, broker, and GC interval are configurable.
110
105
  More than one daemon may run when each uses a distinct port and state directory.
@@ -235,9 +230,8 @@ random public-safe `tmp-…` name), binds it to this session, and marks it **tem
235
230
 
236
231
  If a notice says your plugin and the running daemon are different
237
232
  versions, it is **advisory** — everything still works. Relay it to the user and,
238
- if they want matching versions, tell them: the daemon is shared and is not
239
- restarted automatically, so run `ours-mcp stop` when no other session is
240
- mid-task (the next session starts the new version), or update the lagging side.
233
+ if they want matching versions, tell them to update the lagging package and run
234
+ `ours daemon restart` only when no other session is mid-task.
241
235
  Do **not** stop work, refuse, or restart anything on your own over this.
242
236
 
243
237
  ### Workspace identity pin (`.ours-identity`)
@@ -389,45 +383,21 @@ earlier; `monitor_status` reports availability and the armed identity. A foregro
389
383
  monitor is a blocking tool call, so the session cannot accept another prompt until mail
390
384
  arrives or the user presses Escape.
391
385
 
392
- ## Control plane — bind a monitoring proxy (human oversight of a fleet)
393
-
394
- This is **separate** from the wake-on-mail watch above. The control plane lets a **person's
395
- web-messenger account** (the ours web messenger, shipping as part of the upcoming ours-control-plane)
396
- oversee and command all agents under this host's **Human identity** from a **Control
397
- Panel**: view a **live monitoring feed** of monitored agents' traffic, create agents, edit
398
- their bios **and personas**, toggle each agent's monitoring, open a chat with any agent (the
399
- Human identity commands the agent to mint an invite — no out-of-band step), and remove agents. A
400
- coordinator can also set a worker's local persona via the cluster; the agent still asks the
401
- user before adopting it. All of it rides the same
402
- e2e channels as messages but in a separate control queue agents never see; monitoring bodies
403
- are never written to disk on the host.
404
-
405
- **Prerequisites**
406
- - The **Human identity** exists (`create_root_identity` — the onboarding step). The
407
- proxy binds to the Human identity.
408
- - The messenger account is already a **contact of the Human identity** — do the normal
409
- invite exchange first: bind the Human identity, `generate_invite`, and have the
410
- messenger redeem it (or redeem the messenger's invite with `add_contact`).
411
-
412
- **Binding ceremony (6-digit code, out-of-band)**
413
- 1. "bind my messenger account as the monitoring proxy" →
414
- `bind_monitoring_proxy({ contact: "<the messenger contact>" })`. This automatically
415
- targets the host's Human identity (you do **not** need to be bound as it). It returns a
416
- **6-digit code** (valid 5 minutes, 3 attempts) and shows it **here**.
417
- 2. **Read the code to the user.** They open the messenger → the conversation with the Human identity →
418
- **Control Panel** → enter the code. The code must travel **out-of-band** — reading it off
419
- this terminal is what proves you control both ends. **Never send the code over ours.**
420
- 3. On success the contact becomes the proxy. Confirm with `get_monitoring_status`.
421
-
422
- **Per-agent monitoring is controller-gated.** Once a proxy is bound, the proxy (Control
423
- Panel) turns an agent's monitoring on/off — there is **no local enable/disable tool**. A
424
- monitored agent reports a signed copy of every message it sends/receives to the Human
425
- identity's node, which forwards it to the proxy's feed.
426
-
427
- **Status** — "what's the monitoring/control state" → `get_monitoring_status()` reports the
428
- Human identity's bound proxy (if any), a pending code verification, queued copies/control
429
- requests, and each agent's monitoring ON/off. Works whenever the Human identity exists.
386
+ ## Control plane — human oversight of a fleet
430
387
 
388
+ **NOT AVAILABLE IN THIS RELEASE. Do not offer it, and do not call a tool for it.**
389
+ The `bind_monitoring_proxy` and `get_monitoring_status` MCP tools were removed with the
390
+ daemon-side control plane; there is no tool behind them and a call will fail. Nothing has
391
+ replaced them yet.
392
+
393
+ The capability itself is not cancelled: the monitoring/control surface remains in the
394
+ **protocol core**, untouched, for whenever it is reimplemented. What is gone is this
395
+ plugin's exposure of it as MCP tools.
396
+
397
+ If a user asks to bind a web-messenger account as a monitoring/control proxy, to open a
398
+ Control Panel, or to check monitoring status — say plainly that it is not available in this
399
+ release, and do not improvise a substitute. Per-identity wake-on-mail is a **different**
400
+ feature and still works; it is described above.
431
401
  ## Notes
432
402
 
433
403
  - Identities and their state (contacts, inbox, keys) persist under the selected daemon's
@@ -1,71 +1,26 @@
1
- # ours configuration and daemon profiles
2
-
3
- Each ours daemon owns one port and one state directory. Multiple daemons can run on the
4
- same host when both values are distinct. Configuration resolves as environment variables,
5
- then the file named by `OURS_CONFIG` (otherwise `~/.ours/config.json`), then defaults:
6
-
7
- | Setting | Environment | JSON key | Default |
8
- |---|---|---|---|
9
- | HTTP port | `OURS_PORT` | `port` | `3050` |
10
- | State directory | `OURS_STATE_DIR` | `stateDir` | `~/.ours` |
11
- | Broker | `OURS_BROKER_URL` | `brokerUrl` | bundled public broker |
12
- | API token | `OURS_API_TOKEN` | `apiToken` | owner token file |
13
- | API visibility | `OURS_API_VISIBILITY` | `apiVisibility` | `owner` |
14
- | Auto-start | `OURS_AUTOSTART` | `autoStart` | `false` |
15
-
16
- For live Codex mode, `ours-codex --ours-port <port>` has highest precedence. The launcher
17
- queries `/info`, verifies the authenticated notification API, and propagates that exact
18
- profile to the plugin, hooks, and watcher. It never starts or changes the daemon. A stopped
19
- or incompatible selected daemon is an error.
20
-
21
- Changing daemon configuration is separate operator work. Explain the impact and obtain
22
- explicit consent before editing or restarting anything. A changed state directory selects a
23
- different identity store. Use a distinct `OURS_CONFIG`, port, and state directory for a
24
- second daemon.
25
-
26
- Standard mode and live mode use the same MCP tools. Live mode only adds explicitly armed,
27
- session-scoped wake; it stops with the `ours-codex` session.
28
-
29
- ## Voice-message transcription
30
-
31
- Run `ours-mcp voice-status --json` first. It reports only readiness, provider,
32
- key presence/source, and a missing-field reason; it never returns the key. A
33
- ready result is idempotent: keep it and do not ask for setup again. A not-ready
34
- result should be offered again on every interactive `ours-install` rerun.
35
- Headless/`OURS_ASSUME_YES` runs never prompt and never invent credentials.
36
-
37
- Safest guided setup: ask the user to run `ours-install` in their own terminal.
38
- Its API-key prompt is hidden, it writes `config.json` atomically with mode
39
- `0600`, and it restores the prior file if the daemon cannot reload the change.
40
- Never request a provider key in chat, pass one through an agent tool/command
41
- argument, print the `stt` config block, or test with a real key. Environment-only
42
- operators can set `OURS_STT_PROVIDER`, `OURS_STT_API_KEY`, `OURS_STT_MODEL`,
43
- `OURS_STT_BASE_URL`, and `OURS_STT_LANGUAGE`; environment values override the
44
- file field-by-field.
45
-
46
- Provider requirements:
47
-
48
- - `openai-compatible`: key + explicit `/v1` base URL + model.
49
- - `elevenlabs`: key + model; base URL is optional.
50
- - `deepgram`: key; model/base URL are optional provider defaults.
51
- - `custom`: key + `stt.custom.url`; model is required when the custom template
52
- references it.
53
-
54
- Troubleshooting:
55
-
56
- - “not ready” names the missing field. Do not ask the user to reveal its value.
57
- - If a file edit appears ineffective, check the reported key source and
58
- `OURS_STT_*`; an environment override may shadow the file.
59
- - Config changes require a daemon restart and active sessions may need to bind
60
- their identity again.
61
- - Incoming voice is recognized strictly as an `audio/*` MIME carrying
62
- `x-ours-kind=voice-message`, or the legacy `voice-message-…` audio filename.
63
- Generic audio and connector-specific filename guesses remain ordinary files.
64
- - Telegram fallback preserves the original OGG/Opus bytes and `.ogg` filename.
65
- Its `send_file` MIME and correlated v2 envelope `attachment.mime` must both be
66
- `audio/ogg; x-ours-kind=voice-message`; `attachment.wire_id` identifies the
67
- separately delivered file. Connector-local STT success may remain text-only.
68
- - Oversized audio is saved but not uploaded (daemon default: 5 MiB). Provider
69
- HTTP, timeout, malformed-response, and network failures degrade to a precise
70
- “transcription failed” line with the saved audio path; provider responses are
71
- scrubbed if they echo the configured key.
1
+ # ours daemon configuration
2
+
3
+ ours-mcp is a client of one already-running shared daemon. The operator CLI owns
4
+ configuration and lifecycle:
5
+
6
+ ```sh
7
+ ours config show --json
8
+ ours config setup --port 3050 --state-dir "$HOME/.ours"
9
+ ours daemon start
10
+ ours daemon status --json
11
+ ```
12
+
13
+ The MCP adapter and `ours-codex` use the published SDK's coherent selection. The
14
+ wholly default selection is port 3050 with state directory `~/.ours`. For another
15
+ daemon, set `OURS_CONFIG`, or set matching `OURS_PORT` and `OURS_STATE_DIR`. A
16
+ token or endpoint selection must be paired with its state directory. `/state-dir`
17
+ is verified before credentials are sent.
18
+
19
+ The adapter never starts a daemon and never falls back to an embedded one.
20
+ `OURS_INSTANCE`, `--application`, and old ours-mcp daemon variables are errors.
21
+ Standard and live Codex mode use the same selection; live mode only adds an
22
+ explicitly armed, session-scoped wake monitor.
23
+
24
+ Changing daemon configuration or restarting the shared daemon affects every
25
+ connected application. Explain that blast radius and obtain the user's consent
26
+ before making operator-level changes.
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { pathToFileURL } from 'node:url';
5
6
  import { sendControlCommand } from '../control-server.mjs';
7
+ import { resolveDaemonProfile } from '../profile.mjs';
6
8
 
7
9
  async function defaultFindPin(cwd) {
8
10
  let dir = resolve(cwd || process.cwd());
@@ -29,6 +31,14 @@ const safeUnread = (value) => (Array.isArray(value?.identities) ? value.identiti
29
31
  return [{ name: entry.name, count, files, recent }];
30
32
  });
31
33
 
34
+ async function applicationIdentityNames(env, stateDir) {
35
+ const path = env.OURS_MCP_CONFIG || join(homedir(), '.ours-mcp', 'config.json');
36
+ const value = JSON.parse(await readFile(path, 'utf8'));
37
+ if (value?.version !== 1 || !value.daemons || typeof value.daemons !== 'object') return new Set();
38
+ const identities = value.daemons[resolve(stateDir)]?.identities;
39
+ return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === 'string') : []);
40
+ }
41
+
32
42
  function renderContext(unread, pin) {
33
43
  const lines = [];
34
44
  if (unread.length) {
@@ -62,12 +72,26 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
62
72
  if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
63
73
  await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
64
74
  }
65
- const port = env.OURS_PORT || '3050';
66
- const headers = env.OURS_API_TOKEN ? { 'x-ours-api-token': env.OURS_API_TOKEN } : {};
75
+ let port = env.OURS_PORT || '3050';
76
+ let selectedStateDir = resolve(env.OURS_STATE_DIR || join(homedir(), '.ours'));
77
+ let selectedToken = env.OURS_API_TOKEN || '';
78
+ // Standard Codex does not pass through ours-codex's resolved environment.
79
+ // Resolve the same coherent SDK selection here so SessionStart and
80
+ // UserPromptSubmit inspect the same daemon as the stdio proxy.
81
+ try {
82
+ const selected = await resolveDaemonProfile({ env, fetch: fetchImpl });
83
+ port = String(selected.port);
84
+ selectedToken = selected.token || '';
85
+ selectedStateDir = resolve(selected.stateDir);
86
+ } catch { /* proxy/launcher owns diagnostics; hooks emit a benign no-op */ }
87
+ const headers = selectedToken ? { 'x-ours-api-token': selectedToken } : {};
67
88
  let unread = [];
68
89
  try {
69
90
  const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
70
- if (response.ok) unread = safeUnread(await response.json());
91
+ if (response.ok) {
92
+ const visible = await applicationIdentityNames(env, selectedStateDir);
93
+ unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
94
+ }
71
95
  } catch { /* daemon diagnostics belong to launcher/proxy */ }
72
96
  const pin = await findPin(payload.cwd || process.cwd());
73
97
  const context = renderContext(unread, pin);
@@ -83,4 +107,3 @@ async function main() {
83
107
  }
84
108
 
85
109
  if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) main();
86
-
@@ -20,7 +20,7 @@ const foregroundOffer = (identity) =>
20
20
  `Only after an explicit yes, call get_messages once to drain existing unread mail, then ` +
21
21
  `call foreground_monitor({ identity: ${JSON.stringify(identity)} }).`;
22
22
 
23
- function foregroundWatchProcess(identity) {
23
+ export function foregroundWatchProcess(identity) {
24
24
  try {
25
25
  const cliPath = require.resolve('@ours.network/mcp/dist/cli.js');
26
26
  return { command: process.execPath, args: [cliPath, 'watch', identity] };
package/src/profile.mjs CHANGED
@@ -1,8 +1,7 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { join } from 'node:path';
2
2
  import { homedir } from 'node:os';
3
- import { join, resolve } from 'node:path';
4
3
 
5
- const DEFAULT_PORT = 3050;
4
+ import { assertDaemonStateDir, resolveDaemonConfig } from '@ours.network/sdk';
6
5
 
7
6
  function validPort(value) {
8
7
  const port = Number(value);
@@ -13,11 +12,11 @@ function validPort(value) {
13
12
  export function parseOursArgs(argv = []) {
14
13
  const codexArgs = [];
15
14
  let port;
16
- for (let i = 0; i < argv.length; i += 1) {
17
- const arg = argv[i];
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const arg = argv[index];
18
17
  if (arg === '--ours-port') {
19
- if (argv[i + 1] == null) throw new Error('ours: --ours-port requires a value');
20
- port = validPort(argv[++i]);
18
+ if (argv[index + 1] == null) throw new Error('ours: --ours-port requires a value');
19
+ port = validPort(argv[++index]);
21
20
  } else if (arg.startsWith('--ours-port=')) {
22
21
  port = validPort(arg.slice('--ours-port='.length));
23
22
  } else {
@@ -27,52 +26,41 @@ export function parseOursArgs(argv = []) {
27
26
  return { port, codexArgs };
28
27
  }
29
28
 
30
- async function readJson(path) {
31
- try { return JSON.parse(await readFile(path, 'utf8')); } catch { return {}; }
32
- }
33
-
34
- async function readOwnerToken(stateDir) {
35
- try { return (await readFile(join(stateDir, 'daemon-token'), 'utf8')).trim() || null; } catch { return null; }
36
- }
37
-
38
- export async function resolveDaemonProfile({ argv = [], env = process.env, readConfig, fetch: fetchImpl = globalThis.fetch } = {}) {
29
+ export async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch } = {}) {
39
30
  const parsed = parseOursArgs(argv);
40
- const configPath = env.OURS_CONFIG || join(env.HOME || homedir(), '.ours', 'config.json');
41
- const config = await (readConfig ? readConfig(configPath) : readJson(configPath));
42
- let source = 'default';
43
- let port = DEFAULT_PORT;
44
- if (config?.port != null) { port = validPort(config.port); source = env.OURS_CONFIG ? 'OURS_CONFIG' : 'config'; }
45
- if (env.OURS_PORT != null && env.OURS_PORT !== '') { port = validPort(env.OURS_PORT); source = 'OURS_PORT'; }
46
- if (parsed.port != null) { port = parsed.port; source = '--ours-port'; }
31
+ const selection = resolveDaemonConfig({
32
+ ...(parsed.port == null ? {} : { port: parsed.port }),
33
+ env,
34
+ homeDir: env.HOME || homedir(),
35
+ });
36
+ await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2000 });
47
37
 
48
- const baseUrl = `http://127.0.0.1:${port}`;
49
- let response;
50
- try { response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2000) }); }
51
- catch (error) {
52
- throw new Error(`ours daemon on port ${port} is not reachable; ours-codex never starts it. Start the selected daemon first. (${error?.message || error})`);
53
- }
54
- if (!response.ok) throw new Error(`ours daemon on port ${port} returned HTTP ${response.status}`);
38
+ const baseUrl = selection.baseUrl.value;
39
+ const token = selection.token?.value ?? null;
40
+ const headers = token ? { 'x-ours-api-token': token } : {};
41
+ const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2000) });
42
+ if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
55
43
  const info = await response.json();
56
- if (info?.name !== 'ours' || !Number.isInteger(info?.protocol) || info.protocol < 1 || typeof info?.stateDir !== 'string') {
57
- throw new Error(`incompatible service on port ${port}; expected an ours daemon with notification protocol 1`);
44
+ if (info?.name !== 'ours' || !Number.isInteger(info?.protocol) || info.protocol < 1) {
45
+ throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
46
+ }
47
+ for (const path of ['/identities', '/unread']) {
48
+ const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2000) });
49
+ if (capability.status === 401 || capability.status === 403) {
50
+ throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
51
+ }
52
+ if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
58
53
  }
59
- const stateDir = resolve(info.stateDir);
60
- const token = env.OURS_API_TOKEN?.trim() || config?.apiToken?.trim() || await readOwnerToken(stateDir);
61
- const headers = token ? { 'x-ours-api-token': token } : {};
62
- let capability;
63
- try { capability = await fetchImpl(`${baseUrl}/identities`, { headers, signal: AbortSignal.timeout(2000) }); }
64
- catch (error) { throw new Error(`ours daemon capability check failed: ${error?.message || error}`); }
65
- if (capability.status === 401 || capability.status === 403) throw new Error(`ours daemon authentication failed on port ${port}; supply the matching OURS_API_TOKEN/config`);
66
- if (!capability.ok) throw new Error(`selected daemon lacks the notification API (HTTP ${capability.status})`);
67
- let unread;
68
- try { unread = await fetchImpl(`${baseUrl}/unread`, { headers, signal: AbortSignal.timeout(2000) }); }
69
- catch (error) { throw new Error(`ours daemon unread capability check failed: ${error?.message || error}`); }
70
- if (unread.status === 401 || unread.status === 403) throw new Error(`ours daemon authentication failed on port ${port}; supply the matching OURS_API_TOKEN/config`);
71
- if (!unread.ok) throw new Error(`selected daemon lacks the body-free unread API (HTTP ${unread.status}); install the testing daemon build and restart that daemon explicitly`);
72
54
 
73
55
  return {
74
- port, stateDir, token: token || null,
75
- visibility: env.OURS_API_VISIBILITY || config?.apiVisibility || 'owner',
76
- source, info, baseUrl, configPath, codexArgs: parsed.codexArgs,
56
+ port: selection.port.value,
57
+ stateDir: selection.expectStateDir,
58
+ token,
59
+ visibility: 'owner',
60
+ source: selection.baseUrl.source,
61
+ info,
62
+ baseUrl,
63
+ configPath: selection.configPath?.value ?? join(selection.expectStateDir, 'config.json'),
64
+ codexArgs: parsed.codexArgs,
77
65
  };
78
66
  }