@ours.network/codex 1.2.0-nightly.1 → 1.2.0-nightly.11

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.
@@ -6,7 +6,7 @@ import { dirname, join, resolve } from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { sendControlCommand } from '../control-server.mjs';
8
8
  import { resolveDaemonProfile } from '../profile.mjs';
9
- import { createHostSessionFactory } from '../network-proxy.mjs';
9
+ import { readHostHookState } from '../host-hooks.mjs';
10
10
  import { attachOursClient } from '@ours.network/sdk';
11
11
  import { hostProfileFromEnv } from '../../../core/src/host-client/index.ts';
12
12
 
@@ -35,16 +35,6 @@ const safeUnread = (value) => (Array.isArray(value?.identities) ? value.identiti
35
35
  return [{ name: entry.name, count, files, recent }];
36
36
  });
37
37
 
38
- async function applicationIdentityNames(env, selection) {
39
- const path = env.OURS_MCP_CONFIG || join(homedir(), '.ours-mcp', 'config.json');
40
- const value = JSON.parse(await readFile(path, 'utf8'));
41
- if (value?.version !== 1 || !value.daemons || typeof value.daemons !== 'object') return new Set();
42
- const identities = selection.instanceId
43
- ? value.instances?.[selection.instanceId]?.identities
44
- : value.daemons[resolve(selection.stateDir)]?.identities;
45
- return new Set(Array.isArray(identities) ? identities.filter((name) => typeof name === 'string') : []);
46
- }
47
-
48
38
  function renderContext(unread, pin) {
49
39
  const lines = [];
50
40
  if (unread.length) {
@@ -60,16 +50,7 @@ function renderContext(unread, pin) {
60
50
  return lines.join('\n');
61
51
  }
62
52
 
63
- function applicationIdentitiesFromResource(value) {
64
- const text = value?.contents?.find((item) => item?.uri === 'ours://application-identities')?.text;
65
- const parsed = JSON.parse(text);
66
- if (!Array.isArray(parsed?.identities) || parsed.identities.some((name) => typeof name !== 'string')) {
67
- throw new Error('invalid application identity resource');
68
- }
69
- return new Set(parsed.identities);
70
- }
71
-
72
- export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, networkSessionFactory = createHostSessionFactory } = {}) {
53
+ export async function handleHook(payload, { env = process.env, fetch: fetchImpl = globalThis.fetch, send = sendControlCommand, findPin = defaultFindPin, profileResolver = resolveDaemonProfile, clientFactory = attachOursClient, readHostState = readHostHookState } = {}) {
73
54
  try {
74
55
  const event = payload?.hook_event_name;
75
56
  const socket = env.OURS_CODEX_CONTROL_SOCKET;
@@ -87,9 +68,6 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
87
68
  if (event === 'SessionStart' && socket && capability && payload.session_id && payload.cwd) {
88
69
  await send(socket, capability, { command: 'register_session', sessionId: payload.session_id, threadId: payload.session_id, cwd: payload.cwd });
89
70
  }
90
- let port = env.OURS_PORT || '3050';
91
- let selectedStateDir = resolve(env.OURS_STATE_DIR || join(homedir(), '.ours'));
92
- let selectedToken = env.OURS_API_TOKEN || '';
93
71
  let hostProfile;
94
72
  try { hostProfile = hostProfileFromEnv(env); }
95
73
  catch { return { continue: true }; }
@@ -97,53 +75,13 @@ export async function handleHook(payload, { env = process.env, fetch: fetchImpl
97
75
  const nativeSessionId = payload.session_id;
98
76
  if (typeof nativeSessionId !== 'string' || !nativeSessionId) return { continue: true };
99
77
  const appPath = env.OURS_MCP_CONFIG || join(env.HOME || homedir(), '.ours-mcp', 'config.json');
100
- let session;
101
- try {
102
- const factory = networkSessionFactory({ profile: hostProfile, hostRecordRoot: dirname(appPath), send: async () => {} });
103
- session = await factory(nativeSessionId);
104
- const resource = await session.request({ method: 'resources/read', params: { uri: 'ours://application-identities' } });
105
- const visible = applicationIdentitiesFromResource(resource);
106
- const unread = safeUnread(await session.fileClient.unread()).filter((entry) => visible.has(entry.name));
107
- const pin = await findPin(payload.cwd || process.cwd());
108
- const context = renderContext(unread, pin);
109
- return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
110
- } finally { await session?.close(); }
78
+ const state = await readHostState({ profile: hostProfile, nativeSessionId, applicationPath: appPath });
79
+ const pin = await findPin(payload.cwd || process.cwd());
80
+ const context = renderContext(state.unread.identities, pin);
81
+ return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
111
82
  }
112
- // Standard Codex does not pass through ours-codex's resolved environment.
113
- // Resolve the same coherent SDK selection here so SessionStart and
114
- // UserPromptSubmit inspect the same daemon as the stdio proxy.
115
- try {
116
- const selected = await profileResolver({ env, fetch: fetchImpl });
117
- if (selected.profile) {
118
- let client;
119
- try {
120
- client = await clientFactory({ ...selected.profile, sessionMode: 'external', leaseToken: `codex-hook-${process.pid}`, env: {} });
121
- const visible = await applicationIdentityNames(env, { instanceId: selected.profile.expectedInstanceId });
122
- const unread = safeUnread(await client.unread()).filter((entry) => visible.has(entry.name));
123
- const pin = await findPin(payload.cwd || process.cwd());
124
- const context = renderContext(unread, pin);
125
- return context ? { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } } : { continue: true };
126
- } finally { await client?.close(); }
127
- }
128
- port = String(selected.port);
129
- selectedToken = selected.token || '';
130
- selectedStateDir = resolve(selected.stateDir);
131
- } catch {
132
- /* proxy/launcher owns legacy diagnostics; hooks emit a benign no-op */
133
- }
134
- const headers = selectedToken ? { 'x-ours-api-token': selectedToken } : {};
135
- let unread = [];
136
- try {
137
- const response = await fetchImpl(`http://127.0.0.1:${port}/unread`, { headers, signal: AbortSignal.timeout(1500) });
138
- if (response.ok) {
139
- const visible = await applicationIdentityNames(env, { stateDir: selectedStateDir });
140
- unread = safeUnread(await response.json()).filter((entry) => visible.has(entry.name));
141
- }
142
- } catch { /* daemon diagnostics belong to launcher/proxy */ }
143
- const pin = await findPin(payload.cwd || process.cwd());
144
- const context = renderContext(unread, pin);
145
- if (!context) return { continue: true };
146
- return { continue: true, hookSpecificOutput: { hookEventName: event, additionalContext: context } };
83
+
84
+ return { continue: true };
147
85
  } catch { return { continue: true }; }
148
86
  }
149
87
 
@@ -0,0 +1,25 @@
1
+ import { dirname } from 'node:path';
2
+ import { ApplicationIdentityStore } from '../../core/src/application-identities.ts';
3
+ import { nativeClientForAtRoot } from '../../core/src/host-client/index.ts';
4
+
5
+ /** Body-free hook metadata, scoped to this client's adopted identities. */
6
+ export async function readHostHookState({ profile, nativeSessionId, applicationPath, clientFor = nativeClientForAtRoot }) {
7
+ const store = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: applicationPath });
8
+ const identities = await store.list();
9
+ const visible = new Set(identities);
10
+ const client = await clientFor(profile, nativeSessionId, dirname(applicationPath));
11
+ try {
12
+ const [unread, rows] = await Promise.all([client.unread(), client.listIdentities()]);
13
+ const entries = (Array.isArray(unread?.identities) ? unread.identities : []).flatMap(entry => {
14
+ if (!entry || !visible.has(entry.name)) return [];
15
+ const count = Number.isSafeInteger(entry.count) ? entry.count : 0;
16
+ const files = Number.isSafeInteger(entry.files) ? entry.files : 0;
17
+ if (count <= 0 && files <= 0) return [];
18
+ const recent = (Array.isArray(entry.recent) ? entry.recent.slice(-5) : []).flatMap(message =>
19
+ message && typeof message.from === 'string' ? [{ from: message.from, msg_id: String(message.msg_id ?? '?'), date: typeof message.date === 'string' ? message.date : '' }] : []);
20
+ return [{ name: entry.name, count, files, recent }];
21
+ });
22
+ const bindings = (Array.isArray(rows) ? rows : []).flatMap(row => row && visible.has(row.name) && ['mine', 'other-live'].includes(row.session) ? [row.name] : []);
23
+ return { identities, unread: { identities: entries }, bindings };
24
+ } finally { await client.close(); }
25
+ }
package/src/launcher.mjs CHANGED
@@ -31,15 +31,7 @@ export function launcherEnvironment(env, profile, control) {
31
31
  // call. The bin shim preserves an explicit value instead of replacing it
32
32
  // with whichever child happened to spawn it.
33
33
  };
34
- if (profile.profile) {
35
- for (const key of ['OURS_PORT', 'OURS_API_TOKEN', 'OURS_STATE_DIR', 'OURS_CLIENT_PID', 'OURS_AUTOSTART', 'OURS_DAEMON_ID']) delete out[key];
36
- } else {
37
- out.OURS_PORT = String(profile.port);
38
- out.OURS_AUTOSTART = '0';
39
- out.OURS_CLIENT_PID = String(process.pid);
40
- if (profile.token) out.OURS_API_TOKEN = profile.token;
41
- else delete out.OURS_API_TOKEN;
42
- }
34
+ for (const key of ['OURS_PORT', 'OURS_API_TOKEN', 'OURS_STATE_DIR', 'OURS_CLIENT_PID', 'OURS_AUTOSTART', 'OURS_DAEMON_ID', 'OURS_DAEMON_URL', 'OURS_DAEMON_CREDENTIAL_PATH']) delete out[key];
43
35
  return out;
44
36
  }
45
37
 
package/src/profile.mjs CHANGED
@@ -1,8 +1,6 @@
1
- import { join } from 'node:path';
2
- import { homedir } from 'node:os';
3
1
  import { randomUUID } from 'node:crypto';
4
2
 
5
- import { assertDaemonStateDir, attachOursClient, resolveDaemonConfig } from '@ours.network/sdk';
3
+ import { attachOursClient } from '@ours.network/sdk';
6
4
  import { hostProfileSelectionFromEnv } from '../../core/src/host-client/index.ts';
7
5
 
8
6
  function validPort(value) {
@@ -31,7 +29,6 @@ export function parseOursArgs(argv = []) {
31
29
  export async function resolveDaemonProfile({ argv = [], env = process.env, fetch: fetchImpl = globalThis.fetch, attach = attachOursClient } = {}) {
32
30
  const parsed = parseOursArgs(argv);
33
31
  const hostSelection = hostProfileSelectionFromEnv(env);
34
- if (hostSelection !== null) {
35
32
  const { profile: hostProfile, configPath } = hostSelection;
36
33
  if (parsed.port != null) throw new Error('ours: --ours-port conflicts with host-profile mode');
37
34
  const client = await attach({ ...hostProfile, sessionMode: 'external', leaseToken: randomUUID(), env: {} });
@@ -42,40 +39,4 @@ export async function resolveDaemonProfile({ argv = [], env = process.env, fetch
42
39
  await client.unread();
43
40
  } finally { await client.close(); }
44
41
  return { profile: hostProfile, info, baseUrl: hostProfile.endpoint, configPath, codexArgs: parsed.codexArgs };
45
- }
46
- const selection = resolveDaemonConfig({
47
- ...(parsed.port == null ? {} : { port: parsed.port }),
48
- env,
49
- homeDir: env.HOME || homedir(),
50
- });
51
- await assertDaemonStateDir(selection, { fetch: fetchImpl, timeoutMs: 2000 });
52
-
53
- const baseUrl = selection.baseUrl.value;
54
- const token = selection.token?.value ?? null;
55
- const headers = token ? { 'x-ours-api-token': token } : {};
56
- const response = await fetchImpl(`${baseUrl}/info`, { signal: AbortSignal.timeout(2000) });
57
- if (!response.ok) throw new Error(`ours daemon at ${baseUrl} returned HTTP ${response.status}`);
58
- const info = await response.json();
59
- if (info?.name !== 'ours' || !Number.isInteger(info?.protocol) || info.protocol < 1) {
60
- throw new Error(`incompatible service at ${baseUrl}; expected an ours daemon with notification protocol 1`);
61
- }
62
- for (const path of ['/identities', '/unread']) {
63
- const capability = await fetchImpl(`${baseUrl}${path}`, { headers, signal: AbortSignal.timeout(2000) });
64
- if (capability.status === 401 || capability.status === 403) {
65
- throw new Error(`ours daemon authentication failed at ${baseUrl}; supply the matching coherent selection`);
66
- }
67
- if (!capability.ok) throw new Error(`selected daemon lacks ${path} (HTTP ${capability.status})`);
68
- }
69
-
70
- return {
71
- port: selection.port.value,
72
- stateDir: selection.expectStateDir,
73
- token,
74
- visibility: 'owner',
75
- source: selection.baseUrl.source,
76
- info,
77
- baseUrl,
78
- configPath: selection.configPath?.value ?? join(selection.expectStateDir, 'config.json'),
79
- codexArgs: parsed.codexArgs,
80
- };
81
42
  }
@@ -1,40 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { isAbsolute, join } from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
-
6
- // An explicit container selection is authoritative: failures never fall back
7
- // to a host MCP or host application-identity configuration.
8
- export function containerInvocation(command, args = [], env = process.env) {
9
- const explicitPath = (env.OURS_CONFIG ?? '').trim();
10
- const path = explicitPath || join(homedir(), '.ours', 'config.json');
11
- let profile;
12
- try { profile = JSON.parse(readFileSync(path, 'utf8')); }
13
- catch (error) {
14
- if (!explicitPath && error.code === 'ENOENT') return null;
15
- throw new Error(`cannot read OURS_CONFIG profile ${path}: ${error.message}`);
16
- }
17
- if (!profile || !Object.hasOwn(profile, 'composeFile')) return null;
18
- if (typeof profile.composeFile !== 'string' || !isAbsolute(profile.composeFile)) throw new Error('composeFile must be an absolute path');
19
- if (typeof profile.expectedInstanceId !== 'string' || !profile.expectedInstanceId.trim()) throw new Error('container profile requires expectedInstanceId');
20
- if (profile.composeProject !== undefined && (typeof profile.composeProject !== 'string' || !profile.composeProject.trim())) throw new Error('composeProject must be a nonempty string');
21
- const dockerArgs = ['compose', '-f', profile.composeFile];
22
- if (profile.composeProject !== undefined) dockerArgs.push('-p', profile.composeProject);
23
- dockerArgs.push('exec', '-T');
24
- for (const name of ['CLAUDE_CODE_SESSION_ID', 'OURS_BIND_IDENTITY']) {
25
- if (env[name] !== undefined) dockerArgs.push('-e', `${name}=${env[name]}`);
26
- }
27
- dockerArgs.push('daemon', 'node', '/opt/ours/node_modules/@ours.network/mcp/dist/container.js', profile.expectedInstanceId, command, ...args);
28
- return { command: 'docker', args: dockerArgs, env: { ...env, OURS_DAEMON_ID: profile.expectedInstanceId } };
29
- }
30
-
31
- export function readContainerJson(command, env = process.env) {
32
- const invocation = containerInvocation(command, [], env);
33
- if (!invocation) return null;
34
- const child = spawnSync(invocation.command, invocation.args, { env: invocation.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: 1024 * 1024 });
35
- if (child.error) throw child.error;
36
- if (child.status !== 0) throw new Error(`ours container ${command} failed: ${child.stderr.trim() || child.status}`);
37
- const value = JSON.parse(child.stdout);
38
- if (value === null) throw new Error(`ours container ${command} returned null`);
39
- return value;
40
- }