@cozibud/harness 0.2.1 → 0.3.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 +26 -3
- package/dist/cli.js +12 -14
- package/dist/pairing.js +1 -1
- package/dist/process-driver.js +1 -1
- package/dist/relay-client.d.ts +2 -0
- package/dist/relay-client.js +57 -8
- package/dist/relay-polling.d.ts +12 -0
- package/dist/relay-polling.js +34 -0
- package/dist/relay-protocol.d.ts +10 -3
- package/dist/relay-protocol.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
# `@cozibud/harness`
|
|
2
2
|
|
|
3
|
-
Connect a signed-in Codex CLI to your Cozibud household for
|
|
3
|
+
Connect a signed-in Codex CLI to your Cozibud household for baby-care questions
|
|
4
|
+
and actions in the web and phone apps, and watch voice.
|
|
4
5
|
Keep the computer awake and the outbound relay running while using voice.
|
|
5
|
-
|
|
6
|
-
protocol 2.
|
|
6
|
+
This harness requires Codex CLI 0.160.0 or newer. Watch voice needs a server supporting relay
|
|
7
|
+
protocol 2. Web and phone baby-care conversations require harness 0.3.0,
|
|
8
|
+
Codex CLI 0.160.0 or newer, and a server supporting relay protocol 3.
|
|
9
|
+
This source version still needs its package and server release.
|
|
10
|
+
|
|
11
|
+
## Web and phone baby care
|
|
12
|
+
|
|
13
|
+
Enable Voice and text entry in Settings and choose Codex on your computer.
|
|
14
|
+
Ask about the selected baby's last sleep, follow up about its duration, record
|
|
15
|
+
care, manage timers, or ask for summaries, trends, routines and caregiver coverage.
|
|
16
|
+
The server uses the same baby-care MCP handlers and permissions as other clients.
|
|
17
|
+
Availability of medication and symptom recording follows the server's policy.
|
|
18
|
+
Switch the selected child before asking about another baby.
|
|
19
|
+
|
|
20
|
+
Corrections, deletions, restores, bulk changes and care-plan changes show the
|
|
21
|
+
exact proposal before execution. Choose Confirm change to apply it, or Cancel
|
|
22
|
+
to leave it unchanged. Approval expires after five minutes; a record changed
|
|
23
|
+
elsewhere requires a new proposal. A text message alone cannot approve a proposal.
|
|
24
|
+
|
|
25
|
+
The interpreter has no shell, filesystem, browser, image, memory, plugin or
|
|
26
|
+
external MCP access. It proposes operations to a closed server-side baby-care
|
|
27
|
+
allowlist. Mother tools, feedback and unrelated applications are excluded.
|
|
28
|
+
No separate Codex MCP connection is needed for this path. Your signed-in Codex
|
|
29
|
+
client receives the transcript and bounded baby-care context/results.
|
|
7
30
|
|
|
8
31
|
## Watch voice
|
|
9
32
|
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ensureRelayPairing, pairRelay, relayOrigin } from "./pairing.js";
|
|
|
7
7
|
import { createBuiltInDrivers } from "./process-driver.js";
|
|
8
8
|
import { FileRelayExecutionStore, OutboundRelayBridge, RelayTransportError } from "./relay-client.js";
|
|
9
9
|
import { HarnessRuntime } from "./runtime.js";
|
|
10
|
+
import { RelayPollDelay, relayPause as pause } from "./relay-polling.js";
|
|
10
11
|
import { createHarnessHttpServer } from "./server.js";
|
|
11
12
|
import { DEFAULT_COZIBUD_MCP_URL, setupProvider } from "./setup.js";
|
|
12
13
|
function value(args, name, fallback) {
|
|
@@ -65,8 +66,8 @@ async function relay(args) {
|
|
|
65
66
|
if (available.state === "unavailable")
|
|
66
67
|
throw new Error(available.message);
|
|
67
68
|
const version = available.version?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
68
|
-
if (!version || (Number(version[1]) === 0 && Number(version[2]) <
|
|
69
|
-
throw new Error("
|
|
69
|
+
if (!version || (Number(version[1]) === 0 && Number(version[2]) < 160))
|
|
70
|
+
throw new Error("Baby-care relay requires Codex CLI 0.160.0 or newer. Update Codex, sign in, and retry.");
|
|
70
71
|
const controller = new AbortController();
|
|
71
72
|
process.once("SIGINT", () => controller.abort());
|
|
72
73
|
process.once("SIGTERM", () => controller.abort());
|
|
@@ -75,25 +76,31 @@ async function relay(args) {
|
|
|
75
76
|
const createBridge = () => {
|
|
76
77
|
const namespace = createHash("sha256").update(`${baseUrl}\n${credential}`).digest("hex").slice(0, 20);
|
|
77
78
|
const statePath = process.env.COZIBUD_RELAY_STATE?.trim() || join(homedir(), ".cozibud", `relay-state-${namespace}.json`);
|
|
78
|
-
return new OutboundRelayBridge({ baseUrl, credential, driver, store: new FileRelayExecutionStore(statePath) });
|
|
79
|
+
return new OutboundRelayBridge({ baseUrl, credential, driver, store: new FileRelayExecutionStore(statePath), signal: controller.signal });
|
|
79
80
|
};
|
|
80
81
|
let bridge = createBridge();
|
|
81
82
|
let repaired = false;
|
|
82
83
|
let connected = false;
|
|
83
84
|
let backoffMs = 1_000;
|
|
85
|
+
const pollDelay = new RelayPollDelay();
|
|
84
86
|
while (!controller.signal.aborted) {
|
|
85
87
|
try {
|
|
86
88
|
if (!connected) {
|
|
87
89
|
await bridge.connect();
|
|
88
90
|
connected = true;
|
|
89
91
|
backoffMs = 1_000;
|
|
92
|
+
pollDelay.reset();
|
|
90
93
|
console.log(`Cozibud relay connected with ${driver.label}. No local or LAN listener is open.`);
|
|
91
94
|
}
|
|
92
95
|
const state = await bridge.step();
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const waitMs = pollDelay.after(state, bridge.hasPendingExecution);
|
|
97
|
+
if (waitMs > 0)
|
|
98
|
+
await pause(waitMs, controller.signal);
|
|
95
99
|
}
|
|
96
100
|
catch (error) {
|
|
101
|
+
if (controller.signal.aborted)
|
|
102
|
+
break;
|
|
103
|
+
pollDelay.reset();
|
|
97
104
|
if (error instanceof RelayTransportError && [401, 403, 404].includes(error.status)) {
|
|
98
105
|
await bridge.close();
|
|
99
106
|
if ([401, 403].includes(error.status) && !process.env.COZIBUD_RELAY_CREDENTIAL?.trim() && !repaired && !controller.signal.aborted) {
|
|
@@ -207,12 +214,3 @@ catch (error) {
|
|
|
207
214
|
console.error(error instanceof Error ? error.message : "The local harness failed.");
|
|
208
215
|
process.exitCode = 1;
|
|
209
216
|
}
|
|
210
|
-
function pause(ms, signal) {
|
|
211
|
-
return new Promise(resolve => {
|
|
212
|
-
const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
|
|
213
|
-
const timer = setTimeout(done, ms);
|
|
214
|
-
signal.addEventListener("abort", done, { once: true });
|
|
215
|
-
if (signal.aborted)
|
|
216
|
-
done();
|
|
217
|
-
});
|
|
218
|
-
}
|
package/dist/pairing.js
CHANGED
|
@@ -74,7 +74,7 @@ export async function pairRelay(options) {
|
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
if (result.error?.code === "bridge_limit")
|
|
77
|
-
throw new Error("Your account already has two computers paired. Disconnect one in Cozibud under Settings →
|
|
77
|
+
throw new Error("Your account already has two computers paired. Disconnect one in Cozibud under Settings → Connected devices → Computers, then run pair again.");
|
|
78
78
|
throw new Error("Pairing was denied or expired. Run pair again.");
|
|
79
79
|
}
|
|
80
80
|
throw new Error("Pairing expired. Run pair again.");
|
package/dist/process-driver.js
CHANGED
|
@@ -358,7 +358,7 @@ export const builtInProcessHarnesses = [
|
|
|
358
358
|
command: codexExecutable.command,
|
|
359
359
|
versionArgs: [...codexExecutable.prefixArgs, "--version"],
|
|
360
360
|
args: (input) => [...codexExecutable.prefixArgs, "exec", "--json", "--ephemeral", "--skip-git-repo-check", "--sandbox", "read-only",
|
|
361
|
-
...(input?.interpretation ? ["--ignore-user-config", "--ignore-rules", "-c", "features.shell_tool=false", "-c", "features.apps=false", "-c", "features.multi_agent=false", "-c", "features.hooks=false", "-c", "web_search=\"disabled\""] : ["-c", "mcp_servers.cozibud.required=true"]), "-"],
|
|
361
|
+
...(input?.interpretation ? ["--ignore-user-config", "--ignore-rules", "-c", "features.shell_tool=false", "-c", "features.unified_exec=false", "-c", "tools.view_image=false", "-c", "features.view_image=false", "-c", "features.image_generation=false", "-c", "features.memories=false", "-c", "features.plugins=false", "-c", "features.remote_plugin=false", "-c", "features.skill_mcp_dependency_install=false", "-c", "features.apps=false", "-c", "features.multi_agent=false", "-c", "features.hooks=false", "-c", "web_search=\"disabled\""] : ["-c", "mcp_servers.cozibud.required=true"]), "-"],
|
|
362
362
|
},
|
|
363
363
|
{
|
|
364
364
|
id: "claude",
|
package/dist/relay-client.d.ts
CHANGED
package/dist/relay-client.js
CHANGED
|
@@ -65,10 +65,11 @@ export class OutboundRelayBridge {
|
|
|
65
65
|
constructor(options) {
|
|
66
66
|
this.options = options;
|
|
67
67
|
}
|
|
68
|
+
get hasPendingExecution() { return this.scheduled.size > 0; }
|
|
68
69
|
get fetcher() { return this.options.fetchImpl ?? fetch; }
|
|
69
70
|
headers(extra) { return { authorization: `Bearer ${this.options.credential}`, ...extra }; }
|
|
70
71
|
async json(path, init) {
|
|
71
|
-
const response = await this.fetcher(new URL(path, this.options.baseUrl), { ...init, redirect: "error", signal: AbortSignal.timeout(20_000) });
|
|
72
|
+
const response = await this.fetcher(new URL(path, this.options.baseUrl), { ...init, redirect: "error", signal: this.options.signal ? AbortSignal.any([this.options.signal, AbortSignal.timeout(20_000)]) : AbortSignal.timeout(20_000) });
|
|
72
73
|
if (!response.ok) {
|
|
73
74
|
const code = await response.json().then(value => value.error?.code).catch(() => undefined);
|
|
74
75
|
throw new RelayTransportError(response.status, code ?? `relay_http_${response.status}`);
|
|
@@ -145,13 +146,61 @@ export class OutboundRelayBridge {
|
|
|
145
146
|
try {
|
|
146
147
|
const prompt = command.transcript ?? JSON.stringify(command.action);
|
|
147
148
|
const source = command.source === "structured-action" ? "quick-action" : command.source;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
careRecordIdempotencyKey: `relay:${command.requestId}` }, {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(Math.min(300_000, Math.max(1, Date.parse(command.expiresAt) - Date.now())))]);
|
|
150
|
+
const interpret = (interpretation = command.interpretation) => this.options.driver.run({ prompt, source, client: command.client,
|
|
151
|
+
interpretation, careRecordIdempotencyKey: `relay:${command.requestId}` }, { turnId: command.requestId, signal, emit: () => { } });
|
|
152
|
+
let output;
|
|
153
|
+
if (!command.careTools)
|
|
154
|
+
output = await interpret();
|
|
155
|
+
else {
|
|
156
|
+
if (!command.interpretation || this.options.driver.id !== "codex")
|
|
157
|
+
throw new Error("Baby-care interpretation requires the isolated Codex driver.");
|
|
158
|
+
const results = [];
|
|
159
|
+
let next = command.careTools.confirmed;
|
|
160
|
+
let callId = 0;
|
|
161
|
+
let answerReadback = "";
|
|
162
|
+
for (let round = 0;; round += 1) {
|
|
163
|
+
if (round > 12 || signal.aborted)
|
|
164
|
+
throw new Error("The care request reached its limit. Check the current records before trying again.");
|
|
165
|
+
if (!next) {
|
|
166
|
+
output = await interpret(`${command.interpretation}\n<care_tool_results>${JSON.stringify(results)}</care_tool_results>\n${answerReadback}\nReturn the next care tool request or a final answer as JSON.`);
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(output.text);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error("The care interpreter returned invalid JSON.");
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object")
|
|
175
|
+
throw new Error("The care interpreter returned invalid JSON.");
|
|
176
|
+
const value = parsed;
|
|
177
|
+
if (value.operation !== "tool") {
|
|
178
|
+
if (!["answer", "clarify", "refuse"].includes(value.operation ?? "") || typeof value.text !== "string" || !value.text.trim())
|
|
179
|
+
throw new Error("The care interpreter returned an invalid answer.");
|
|
180
|
+
if (value.operation === "answer" && callId === 0) {
|
|
181
|
+
answerReadback = "Your answer has no fresh server readback for this message. Return a care tool request to check the referenced record now; prior conversation results alone cannot support a final answer.";
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (typeof value.name !== "string" || !value.arguments || Array.isArray(value.arguments) || typeof value.arguments !== "object")
|
|
187
|
+
throw new Error("The care interpreter returned an invalid tool request.");
|
|
188
|
+
next = { name: value.name, arguments: value.arguments };
|
|
189
|
+
}
|
|
190
|
+
if (++callId > 12)
|
|
191
|
+
throw new Error("The care request reached its tool limit.");
|
|
192
|
+
const result = await this.json(`/api/watch-relay/v1/deliveries/${command.deliveryId}/care-tools`, { method: "POST", headers: this.headers({ "content-type": "application/json" }),
|
|
193
|
+
body: JSON.stringify({ requestId: command.requestId, connectionId: this.connectionId, callId, ...next }) });
|
|
194
|
+
if (result.retrySameCall)
|
|
195
|
+
callId -= 1;
|
|
196
|
+
results.push({ name: next.name, ...result });
|
|
197
|
+
next = undefined;
|
|
198
|
+
if (result.status === "confirmation_required") {
|
|
199
|
+
output = { text: JSON.stringify({ operation: "clarify", text: "Review this proposed change before applying it." }) };
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
155
204
|
terminal = { state: "completed", modelOutput: output.text, updatedAt: new Date().toISOString() };
|
|
156
205
|
}
|
|
157
206
|
catch (error) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Short polls retain the server's 45-second connection lease. Only a quiet,
|
|
2
|
+
* empty bridge backs off: active provider work keeps the existing one-second
|
|
3
|
+
* cancellation cadence. New commands, cancellation and reconnect reset it.
|
|
4
|
+
* No request is deliberately held open. Idle discovery may wait up to 2.5s. The cap stays below ordinary client keep-alive timeouts to avoid turning idle polls into repeated TLS handshakes. */
|
|
5
|
+
export declare class RelayPollDelay {
|
|
6
|
+
private readonly random;
|
|
7
|
+
private nextIdleMs;
|
|
8
|
+
constructor(random?: () => number);
|
|
9
|
+
reset(): void;
|
|
10
|
+
after(state: "idle" | "command" | "cancel", hasPendingExecution: boolean): number;
|
|
11
|
+
}
|
|
12
|
+
export declare function relayPause(ms: number, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Short polls retain the server's 45-second connection lease. Only a quiet,
|
|
2
|
+
* empty bridge backs off: active provider work keeps the existing one-second
|
|
3
|
+
* cancellation cadence. New commands, cancellation and reconnect reset it.
|
|
4
|
+
* No request is deliberately held open. Idle discovery may wait up to 2.5s. The cap stays below ordinary client keep-alive timeouts to avoid turning idle polls into repeated TLS handshakes. */
|
|
5
|
+
export class RelayPollDelay {
|
|
6
|
+
random;
|
|
7
|
+
nextIdleMs = 1_000;
|
|
8
|
+
constructor(random = Math.random) {
|
|
9
|
+
this.random = random;
|
|
10
|
+
}
|
|
11
|
+
reset() { this.nextIdleMs = 1_000; }
|
|
12
|
+
after(state, hasPendingExecution) {
|
|
13
|
+
if (state !== "idle") {
|
|
14
|
+
this.reset();
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (hasPendingExecution) {
|
|
18
|
+
this.reset();
|
|
19
|
+
return 1_000;
|
|
20
|
+
}
|
|
21
|
+
const delay = Math.round(this.nextIdleMs * (0.8 + this.random() * 0.2));
|
|
22
|
+
this.nextIdleMs = Math.min(2_500, this.nextIdleMs * 2);
|
|
23
|
+
return delay;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function relayPause(ms, signal) {
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
|
|
29
|
+
const timer = setTimeout(done, ms);
|
|
30
|
+
signal.addEventListener("abort", done, { once: true });
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
done();
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/relay-protocol.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Versioned wire values consumed by the outbound Cozibud relay client. */
|
|
2
|
-
export declare const WATCH_RELAY_PROTOCOL: "cozibud.watch-relay.
|
|
3
|
-
export declare const WATCH_RELAY_PROTOCOL_VERSION:
|
|
2
|
+
export declare const WATCH_RELAY_PROTOCOL: "cozibud.watch-relay.v3";
|
|
3
|
+
export declare const WATCH_RELAY_PROTOCOL_VERSION: 3;
|
|
4
4
|
export type WatchRelayCommand = Readonly<{
|
|
5
|
-
protocol: typeof WATCH_RELAY_PROTOCOL;
|
|
5
|
+
protocol: typeof WATCH_RELAY_PROTOCOL | "cozibud.watch-relay.v2";
|
|
6
6
|
requestId: string;
|
|
7
7
|
deliveryId: string;
|
|
8
8
|
attempt: number;
|
|
@@ -15,4 +15,11 @@ export type WatchRelayCommand = Readonly<{
|
|
|
15
15
|
}>;
|
|
16
16
|
expiresAt: string;
|
|
17
17
|
interpretation?: string;
|
|
18
|
+
careTools?: Readonly<{
|
|
19
|
+
enabled: true;
|
|
20
|
+
confirmed?: {
|
|
21
|
+
name: string;
|
|
22
|
+
arguments: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
18
25
|
}>;
|
package/dist/relay-protocol.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Versioned wire values consumed by the outbound Cozibud relay client. */
|
|
2
|
-
export const WATCH_RELAY_PROTOCOL = "cozibud.watch-relay.
|
|
3
|
-
export const WATCH_RELAY_PROTOCOL_VERSION =
|
|
2
|
+
export const WATCH_RELAY_PROTOCOL = "cozibud.watch-relay.v3";
|
|
3
|
+
export const WATCH_RELAY_PROTOCOL_VERSION = 3;
|