@ouro.bot/cli 0.1.0-alpha.925 → 0.1.0-alpha.926
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/changelog.json +10 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-exec.js +5 -1
- package/dist/heart/daemon/cli-help.js +2 -2
- package/dist/heart/daemon/cli-parse.js +8 -3
- package/dist/heart/frontend-session-service.js +8 -18
- package/dist/heart/frontend-socket-client.js +11 -2
- package/dist/heart/frontend-socket.js +37 -28
- package/dist/heart/frontend-turn-errors.js +20 -0
- package/dist/senses/shepherd/attention-page.js +163 -0
- package/dist/senses/shepherd/attention-sidebar.js +74 -0
- package/dist/senses/shepherd/attention.js +280 -0
- package/dist/senses/shepherd/cmux.js +2 -0
- package/dist/senses/shepherd/composer.js +13 -2
- package/dist/senses/shepherd/surface.js +4 -0
- package/dist/senses/shepherd-entry.js +21 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.926",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Open a compact independent Ouro session composer with short About labels, draft preservation, canonical replies and no blind resubmission.",
|
|
8
|
+
"Install a managed cmux Swift Talk to Ouro entry that opens a browser split without moving coding focus; pin each conversation independently and retain the existing family-only terminal authority.",
|
|
9
|
+
"Preserve the original session and request identity through lost acknowledgements and listener restarts; refresh choices without retargeting, recover definitively refused starts, and keep one-off web views from replacing the managed native entry.",
|
|
10
|
+
"Validate the UTF-8 request budget before arming a send, keep explicitly oversized unsent drafts editable, and decode multibyte HTTP messages without corruption; an earlier unknown acknowledgement still retains its original identity.",
|
|
11
|
+
"Trace independent view generation, native entry generation and expected frontend conflicts without logging entry credentials, nonces, session keys or human feedback."
|
|
12
|
+
]
|
|
13
|
+
},
|
|
4
14
|
{
|
|
5
15
|
"version": "0.1.0-alpha.925",
|
|
6
16
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.926</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -7912,7 +7912,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7912
7912
|
const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../frontend-socket")));
|
|
7913
7913
|
const socketPath = frontendSocketPathForDaemon(deps.socketPath);
|
|
7914
7914
|
if (deps.startShepherdChat) {
|
|
7915
|
-
await deps.startShepherdChat({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
|
|
7915
|
+
await deps.startShepherdChat({ agent: command.agent, ...(command.session ? { session: command.session } : {}), ...(command.web ? { web: true } : {}), socketPath });
|
|
7916
|
+
}
|
|
7917
|
+
else if (command.web) {
|
|
7918
|
+
const { runShepherdAttention } = await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/attention")));
|
|
7919
|
+
await runShepherdAttention({ agent: command.agent, ...(command.session ? { session: command.session } : {}), socketPath });
|
|
7916
7920
|
}
|
|
7917
7921
|
else {
|
|
7918
7922
|
const { runShepherdComposer } = await Promise.resolve().then(() => __importStar(require("../../senses/shepherd/composer")));
|
|
@@ -120,8 +120,8 @@ exports.COMMAND_REGISTRY = {
|
|
|
120
120
|
},
|
|
121
121
|
shepherd: {
|
|
122
122
|
category: "Agents",
|
|
123
|
-
description: "Open an independent Ouro composer for one coding terminal.
|
|
124
|
-
usage: "ouro shepherd chat --agent <name> [--session <UUID>]",
|
|
123
|
+
description: "Open an independent Ouro composer for one coding terminal. --web opens the compact graphical view in cmux; the managed cmux Shepherd sense also installs a native Talk to Ouro button. Without --web, /cancel cancels only Ouro's turn and /quit exits. Not an input hook in the coding agent.",
|
|
124
|
+
usage: "ouro shepherd chat --agent <name> [--session <UUID>] [--web]",
|
|
125
125
|
example: "ouro shepherd chat --agent ouroboros",
|
|
126
126
|
subcommands: ["chat"],
|
|
127
127
|
},
|
|
@@ -3122,22 +3122,27 @@ function parseOuroCommand(args) {
|
|
|
3122
3122
|
if (head === "mcp")
|
|
3123
3123
|
return parseMcpCommand(args.slice(1));
|
|
3124
3124
|
if (head === "shepherd") {
|
|
3125
|
-
const usage = "Usage: ouro shepherd chat --agent <name> [--session <UUID>]";
|
|
3125
|
+
const usage = "Usage: ouro shepherd chat --agent <name> [--session <UUID>] [--web]";
|
|
3126
3126
|
if (args[1] !== "chat")
|
|
3127
3127
|
throw new Error(usage);
|
|
3128
3128
|
const values = new Map();
|
|
3129
|
-
for (let i = 2; i < args.length; i +=
|
|
3129
|
+
for (let i = 2; i < args.length; i += 1) {
|
|
3130
3130
|
const flag = args[i];
|
|
3131
|
+
if (flag === "--web" && !values.has(flag)) {
|
|
3132
|
+
values.set(flag, "true");
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3131
3135
|
const value = args[i + 1];
|
|
3132
3136
|
if (!["--agent", "--session"].includes(flag) || values.has(flag) || !value || value.startsWith("--"))
|
|
3133
3137
|
throw new Error(usage);
|
|
3134
3138
|
values.set(flag, value);
|
|
3139
|
+
i += 1;
|
|
3135
3140
|
}
|
|
3136
3141
|
const agent = values.get("--agent");
|
|
3137
3142
|
const session = values.get("--session");
|
|
3138
3143
|
if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent) || (session !== undefined && !surface_1.SHEPHERD_UUID.test(session)))
|
|
3139
3144
|
throw new Error(usage);
|
|
3140
|
-
return { kind: "shepherd.chat", agent, ...(session ? { session } : {}) };
|
|
3145
|
+
return { kind: "shepherd.chat", agent, ...(session ? { session } : {}), ...(values.has("--web") ? { web: true } : {}) };
|
|
3141
3146
|
}
|
|
3142
3147
|
if (head === "voice")
|
|
3143
3148
|
return parseVoiceCommand(args.slice(1));
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FrontendSessionService = exports.
|
|
3
|
+
exports.FrontendSessionService = exports.FrontendTurnConflictError = exports.FrontendSessionConflictError = void 0;
|
|
4
4
|
const shared_turn_1 = require("../senses/shared-turn");
|
|
5
5
|
const runtime_1 = require("../nerves/runtime");
|
|
6
6
|
const surface_1 = require("../senses/shepherd/surface");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
exports.FrontendTurnConflictError = FrontendTurnConflictError;
|
|
14
|
-
class FrontendSessionConflictError extends Error {
|
|
15
|
-
constructor(sessionKey) {
|
|
16
|
-
super(`frontend session already has an active turn: ${sessionKey}`);
|
|
17
|
-
this.name = "FrontendSessionConflictError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
exports.FrontendSessionConflictError = FrontendSessionConflictError;
|
|
7
|
+
const frontend_turn_errors_1 = require("./frontend-turn-errors");
|
|
8
|
+
var frontend_turn_errors_2 = require("./frontend-turn-errors");
|
|
9
|
+
Object.defineProperty(exports, "FrontendSessionConflictError", { enumerable: true, get: function () { return frontend_turn_errors_2.FrontendSessionConflictError; } });
|
|
10
|
+
Object.defineProperty(exports, "FrontendTurnConflictError", { enumerable: true, get: function () { return frontend_turn_errors_2.FrontendTurnConflictError; } });
|
|
21
11
|
const JOURNALED_FRONTEND_EVENTS = new Set([
|
|
22
12
|
"assistant_delivery",
|
|
23
13
|
"tool_started",
|
|
@@ -144,12 +134,12 @@ class FrontendSessionService {
|
|
|
144
134
|
...(request.shepherdSession ? { shepherdSession: Object.freeze({ ...request.shepherdSession }) } : {}),
|
|
145
135
|
};
|
|
146
136
|
if (this.activeTurns.has(turnId))
|
|
147
|
-
throw new FrontendTurnConflictError(turnId);
|
|
137
|
+
throw new frontend_turn_errors_1.FrontendTurnConflictError(turnId);
|
|
148
138
|
if (this.journal && this.turnStatus(normalized, turnId).state !== "unknown")
|
|
149
|
-
throw new FrontendTurnConflictError(turnId);
|
|
139
|
+
throw new frontend_turn_errors_1.FrontendTurnConflictError(turnId);
|
|
150
140
|
const sessionIdentity = [normalized.agent, normalized.friendId, normalized.channel, normalized.sessionKey].join("\0");
|
|
151
141
|
if (this.activeSessions.has(sessionIdentity))
|
|
152
|
-
throw new FrontendSessionConflictError(normalized.sessionKey);
|
|
142
|
+
throw new frontend_turn_errors_1.FrontendSessionConflictError(normalized.sessionKey);
|
|
153
143
|
const controller = new AbortController();
|
|
154
144
|
this.activeTurns.set(turnId, controller);
|
|
155
145
|
this.turnRefs.set(turnId, normalized);
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SocketFrontendClient = void 0;
|
|
36
|
+
exports.SocketFrontendClient = exports.FrontendRequestError = void 0;
|
|
37
37
|
const net = __importStar(require("node:net"));
|
|
38
38
|
const runtime_1 = require("../nerves/runtime");
|
|
39
39
|
// A dropped or refused frontend socket is a transient transport condition, not a
|
|
@@ -45,6 +45,15 @@ function markTransientTransport(error) {
|
|
|
45
45
|
function transientTransportError(message) {
|
|
46
46
|
return markTransientTransport(new Error(message));
|
|
47
47
|
}
|
|
48
|
+
class FrontendRequestError extends Error {
|
|
49
|
+
code;
|
|
50
|
+
constructor(code, message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.name = "FrontendRequestError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.FrontendRequestError = FrontendRequestError;
|
|
48
57
|
class SocketFrontendClient {
|
|
49
58
|
socketPath;
|
|
50
59
|
pending = new Map();
|
|
@@ -143,7 +152,7 @@ class SocketFrontendClient {
|
|
|
143
152
|
continue;
|
|
144
153
|
this.pending.delete(frame.id);
|
|
145
154
|
if (frame.ok === false) {
|
|
146
|
-
pending.reject(new
|
|
155
|
+
pending.reject(new FrontendRequestError(String(frame.error?.code ?? "request_failed"), String(frame.error?.message ?? "frontend request failed")));
|
|
147
156
|
}
|
|
148
157
|
else {
|
|
149
158
|
pending.resolve(frame.result);
|
|
@@ -41,6 +41,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
41
41
|
const net = __importStar(require("node:net"));
|
|
42
42
|
const path = __importStar(require("node:path"));
|
|
43
43
|
const readline = __importStar(require("node:readline"));
|
|
44
|
+
const frontend_turn_errors_1 = require("./frontend-turn-errors");
|
|
44
45
|
const runtime_1 = require("../nerves/runtime");
|
|
45
46
|
const PROTOCOL_VERSION = 1;
|
|
46
47
|
const MAX_SOCKET_PATH_BYTES = 100;
|
|
@@ -302,36 +303,44 @@ async function startFrontendSocketServer(options) {
|
|
|
302
303
|
return;
|
|
303
304
|
}
|
|
304
305
|
if (method === "turn.start" || method === "turn.start.observe-only") {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
306
|
+
let prepared;
|
|
307
|
+
// Only preparation can be definitively refused; duplicate IDs must still reconcile.
|
|
308
|
+
try {
|
|
309
|
+
const observeOnly = method === "turn.start.observe-only";
|
|
310
|
+
if (params.toolMode !== undefined) {
|
|
311
|
+
throw new Error("toolMode is unsupported; use turn.start.observe-only");
|
|
312
|
+
}
|
|
313
|
+
if (observeOnly && params.runtimeMcpServers !== undefined) {
|
|
314
|
+
throw new Error("turn.start.observe-only cannot include runtimeMcpServers");
|
|
315
|
+
}
|
|
316
|
+
const runtimeMcpServers = validatedRuntimeMcpServers(params.runtimeMcpServers);
|
|
317
|
+
const shepherdSession = params.shepherdSession === undefined ? undefined : await options.service.resolveShepherdSession(requiredString(params.agent, "agent"), requiredString(params.friendId, "friendId"), params.shepherdSession, params.composerTty === undefined ? undefined : requiredString(params.composerTty, "composerTty"));
|
|
318
|
+
if (shepherdSession && (observeOnly || params.channel !== "cli" || params.sessionKey !== `shepherd:${shepherdSession.id}`)) {
|
|
319
|
+
throw new Error("Shepherd routing requires a cli turn and the exact per-surface session key");
|
|
320
|
+
}
|
|
321
|
+
const turnRequest = {
|
|
322
|
+
turnId: requiredString(params.turnId, "turnId"),
|
|
323
|
+
agent: requiredString(params.agent, "agent"),
|
|
324
|
+
friendId: requiredString(params.friendId, "friendId"),
|
|
325
|
+
channel: requiredString(params.channel, "channel"),
|
|
326
|
+
sessionKey: requiredString(params.sessionKey, "sessionKey"),
|
|
327
|
+
message: (requiredString(params.message, "message"), params.message),
|
|
328
|
+
...(shepherdSession ? { shepherdSession } : {}),
|
|
329
|
+
...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
|
|
330
|
+
...(runtimeMcpServers ? { runtimeMcpServers } : {}),
|
|
331
|
+
};
|
|
332
|
+
// Validation may await the host while this connection exits. Ownership must
|
|
333
|
+
// never be acquired after cleanup has already drained this client's turns.
|
|
334
|
+
if (client.closed || client.socket.destroyed || stopped)
|
|
335
|
+
return;
|
|
336
|
+
prepared = options.service.prepareTurn(turnRequest);
|
|
316
337
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
agent: requiredString(params.agent, "agent"),
|
|
320
|
-
friendId: requiredString(params.friendId, "friendId"),
|
|
321
|
-
channel: requiredString(params.channel, "channel"),
|
|
322
|
-
sessionKey: requiredString(params.sessionKey, "sessionKey"),
|
|
323
|
-
message: (requiredString(params.message, "message"), params.message),
|
|
324
|
-
...(shepherdSession ? { shepherdSession } : {}),
|
|
325
|
-
...(observeOnly ? { disableTools: true, ephemeral: true } : {}),
|
|
326
|
-
...(runtimeMcpServers ? { runtimeMcpServers } : {}),
|
|
327
|
-
};
|
|
328
|
-
// Validation may await the host while this connection exits. Ownership must
|
|
329
|
-
// never be acquired after cleanup has already drained this client's turns.
|
|
330
|
-
if (client.closed || client.socket.destroyed || stopped)
|
|
338
|
+
catch (error) {
|
|
339
|
+
client.writer.send(errorResponse(id, error instanceof frontend_turn_errors_1.FrontendTurnConflictError ? "turn_conflict" : "turn_rejected", error instanceof Error ? error.message : String(error)));
|
|
331
340
|
return;
|
|
332
|
-
|
|
333
|
-
client.ownedTurnIds.add(
|
|
334
|
-
client.writer.send(response(id, { accepted: true, turnId:
|
|
341
|
+
}
|
|
342
|
+
client.ownedTurnIds.add(prepared.request.turnId);
|
|
343
|
+
client.writer.send(response(id, { accepted: true, turnId: prepared.request.turnId }));
|
|
335
344
|
queueMicrotask(() => {
|
|
336
345
|
void options.service.runPreparedTurn(prepared).catch(() => undefined);
|
|
337
346
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FrontendSessionConflictError = exports.FrontendTurnConflictError = void 0;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
class FrontendTurnConflictError extends Error {
|
|
6
|
+
constructor(turnId) {
|
|
7
|
+
super(`frontend turn already exists: ${turnId}`);
|
|
8
|
+
this.name = "FrontendTurnConflictError";
|
|
9
|
+
(0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.frontend_turn_conflict", message: "frontend request named an already-owned turn" });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.FrontendTurnConflictError = FrontendTurnConflictError;
|
|
13
|
+
class FrontendSessionConflictError extends Error {
|
|
14
|
+
constructor(sessionKey) {
|
|
15
|
+
super(`frontend session already has an active turn: ${sessionKey}`);
|
|
16
|
+
this.name = "FrontendSessionConflictError";
|
|
17
|
+
(0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.frontend_session_conflict", message: "frontend session refused a competing turn" });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.FrontendSessionConflictError = FrontendSessionConflictError;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_ATTENTION_BODY_BYTES = void 0;
|
|
4
|
+
exports.attentionPage = attentionPage;
|
|
5
|
+
exports.MAX_ATTENTION_BODY_BYTES = 16_384;
|
|
6
|
+
/** No model content or target metadata is interpolated into executable HTML. */
|
|
7
|
+
function attentionPage(nonce) {
|
|
8
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_page_rendered", message: "rendered the independent session attention view" });
|
|
9
|
+
return `<!doctype html>
|
|
10
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ouro</title>
|
|
11
|
+
<style nonce="${nonce}">
|
|
12
|
+
:root{color-scheme:dark;font:15px -apple-system,BlinkMacSystemFont,sans-serif;color:#eee;background:#17191d}
|
|
13
|
+
*{box-sizing:border-box}body{margin:0}main{max-width:680px;height:100dvh;min-height:340px;margin:auto;display:flex;flex-direction:column;padding:20px;gap:16px}
|
|
14
|
+
header{display:flex;align-items:center;gap:10px}h1{font-size:21px;margin:0;letter-spacing:-.5px}.badge{font-size:12px;color:#9da4ae}
|
|
15
|
+
label{display:flex;align-items:center;gap:10px;color:#aab1bb;font-size:13px}select{min-width:0;flex:1}
|
|
16
|
+
select,textarea,button{font:inherit;color:inherit;border:1px solid #3b4049;background:#22262c;border-radius:10px;padding:10px}
|
|
17
|
+
textarea{width:100%;resize:none;min-height:82px;outline:none}textarea:focus{border-color:#9bafff}
|
|
18
|
+
#conversation{flex:1;overflow:auto;display:flex;flex-direction:column;gap:14px;min-height:70px}
|
|
19
|
+
.message{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.55;margin:0}.you{align-self:flex-end;background:#2c354a;padding:10px 14px;border-radius:14px;max-width:90%}.notice{font-size:13px;color:#e0b275}
|
|
20
|
+
#empty{color:#9da4ae;line-height:1.6;margin:auto 0}footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}.actions{display:flex;gap:8px;margin-left:auto}
|
|
21
|
+
button{cursor:pointer}button:disabled{opacity:.45;cursor:default}#send{background:#a8b8ff;color:#141823;border-color:transparent;font-weight:600}
|
|
22
|
+
#status{font-size:12px;color:#9da4ae}#error{color:#e0b275;font-size:13px;margin:0}form{display:flex;flex-direction:column;gap:8px}
|
|
23
|
+
</style></head><body><main>
|
|
24
|
+
<header><h1>Ouro</h1><span class="badge">Your session companion</span></header>
|
|
25
|
+
<label for="target">About<select id="target" aria-label="Session to discuss"></select><button id="refresh" type="button" aria-label="Refresh session choices">Refresh</button></label>
|
|
26
|
+
<section id="conversation" aria-live="polite"><p id="empty">Ask Ouro about this session, steer the work, or correct a handback.<br>Your coding agent's input stays untouched.</p></section>
|
|
27
|
+
<p id="error" role="alert"></p><form id="compose"><textarea id="draft" aria-label="Message Ouro" placeholder="Message Ouro..." maxlength="12000"></textarea>
|
|
28
|
+
<footer><span id="status">Independent of the coding agent</span><div class="actions"><button id="recover" type="button" hidden>Reconnect</button><button id="cancel" type="button" hidden>Cancel Ouro</button><button id="send" type="submit" disabled>Send</button></div></footer></form>
|
|
29
|
+
</main><script nonce="${nonce}">
|
|
30
|
+
const fragment=new URLSearchParams(location.hash.slice(1));const initialTarget=fragment.get('session');const token=fragment.get('token')||location.hash.slice(1).split('&')[0]||sessionStorage.getItem('ouroAttentionToken');
|
|
31
|
+
if(token){sessionStorage.setItem('ouroAttentionToken',token);history.replaceState(null,'',location.pathname);}
|
|
32
|
+
const view=sessionStorage.getItem('ouroAttentionView')||crypto.randomUUID();sessionStorage.setItem('ouroAttentionView',view);
|
|
33
|
+
const el=id=>document.getElementById(id);
|
|
34
|
+
let state=null,pending=null,sending=false,targetsSignature='',messagesSignature='';
|
|
35
|
+
let bindingReady=sessionStorage.getItem('ouroAttentionInitialized')==='true',pendingFault=false;
|
|
36
|
+
try {
|
|
37
|
+
pending=JSON.parse(sessionStorage.getItem('ouroAttentionPending')||'null');
|
|
38
|
+
if(pending&&(typeof pending.id!=='string'||typeof pending.target!=='string'||typeof pending.message!=='string'))throw Error('Stored send identity is incomplete.');
|
|
39
|
+
} catch(error) {
|
|
40
|
+
pendingFault=true;
|
|
41
|
+
el('error').textContent=error.message+' Check this conversation before resending; its request identity has not been discarded.';
|
|
42
|
+
}
|
|
43
|
+
let pendingWasUncertain=!!pending;
|
|
44
|
+
el('draft').value=sessionStorage.getItem('ouroAttentionDraft')||'';el('draft').oninput=()=>sessionStorage.setItem('ouroAttentionDraft',el('draft').value);
|
|
45
|
+
async function api(route,body) {
|
|
46
|
+
const response=await fetch(route,{
|
|
47
|
+
method:body?'POST':'GET',
|
|
48
|
+
headers:{'X-Ouro-Attention':token||'','X-Ouro-View':view,...(pending?{'X-Ouro-Pending':pending.id}:{}),...(body?{'Content-Type':'application/json'}:{})},
|
|
49
|
+
...(body?{body:JSON.stringify(body)}:{})
|
|
50
|
+
});
|
|
51
|
+
const data=await response.json();
|
|
52
|
+
if(!response.ok) {const error=Error(data.error||'Ouro could not accept this action.');error.status=response.status;throw error;}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
function acknowledge(submitted) {
|
|
56
|
+
if(!pending||pending.id!==submitted.id)return;
|
|
57
|
+
pending=null;
|
|
58
|
+
pendingWasUncertain=false;
|
|
59
|
+
sessionStorage.removeItem('ouroAttentionPending');
|
|
60
|
+
if(el('draft').value===submitted.message) {
|
|
61
|
+
el('draft').value='';
|
|
62
|
+
sessionStorage.removeItem('ouroAttentionDraft');
|
|
63
|
+
}
|
|
64
|
+
el('error').textContent='';
|
|
65
|
+
}
|
|
66
|
+
function render(next) {
|
|
67
|
+
state=next;
|
|
68
|
+
if(pending&&next.acknowledged?.id===pending.id&&next.acknowledged.target===pending.target)acknowledge(pending);
|
|
69
|
+
const picker=el('target'),signature=JSON.stringify(next.targets);
|
|
70
|
+
if(signature!==targetsSignature) {
|
|
71
|
+
targetsSignature=signature;picker.replaceChildren();
|
|
72
|
+
const prompt=document.createElement('option');prompt.value='';prompt.textContent='Choose a session';picker.append(prompt);
|
|
73
|
+
for(const target of next.targets) {
|
|
74
|
+
const option=document.createElement('option');option.value=target.id;option.textContent=target.label;picker.append(option);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
picker.value=bindingReady?next.target||'':'';
|
|
78
|
+
picker.disabled=next.active||sending||!!pending||pendingFault;
|
|
79
|
+
const conversation=el('conversation'),nextMessages=JSON.stringify(next.messages);
|
|
80
|
+
if(next.messages.length&&nextMessages!==messagesSignature) {
|
|
81
|
+
const atBottom=conversation.scrollHeight-conversation.scrollTop-conversation.clientHeight<60;
|
|
82
|
+
conversation.replaceChildren();
|
|
83
|
+
for(const message of next.messages) {
|
|
84
|
+
const p=document.createElement('p');p.className='message '+message.role;p.textContent=message.text;conversation.append(p);
|
|
85
|
+
}
|
|
86
|
+
if(atBottom)conversation.scrollTop=conversation.scrollHeight;
|
|
87
|
+
} else if(!next.messages.length&&!el('empty')) {
|
|
88
|
+
conversation.replaceChildren();
|
|
89
|
+
const p=document.createElement('p');p.id='empty';p.textContent="Ask Ouro about this session. Your coding agent's input stays untouched.";conversation.append(p);
|
|
90
|
+
}
|
|
91
|
+
messagesSignature=nextMessages;
|
|
92
|
+
const available=next.targets.some(target=>target.id===next.target);
|
|
93
|
+
el('send').disabled=next.active||sending||!bindingReady||!available||pendingFault||!!(pending&&pending.target!==next.target);
|
|
94
|
+
el('cancel').hidden=!next.active;el('recover').hidden=!next.active&&!pending;
|
|
95
|
+
el('status').textContent=next.active?'Ouro is replying...':!next.targets.length?'No coding sessions available':!available?'Choose an available session':'Independent of the coding agent';
|
|
96
|
+
}
|
|
97
|
+
async function refresh() {
|
|
98
|
+
try {render(await api('/state'));}
|
|
99
|
+
catch(error) {el('error').textContent=error.message;el('send').disabled=true;}
|
|
100
|
+
}
|
|
101
|
+
async function select(target) {
|
|
102
|
+
const next=await api('/select',{target});
|
|
103
|
+
bindingReady=true;sessionStorage.setItem('ouroAttentionInitialized','true');render(next);
|
|
104
|
+
}
|
|
105
|
+
el('target').onchange=async()=>{
|
|
106
|
+
if(pending||pendingFault||sending) {el('error').textContent='Resolve the original message acknowledgement before changing About.';if(state)render(state);return;}
|
|
107
|
+
try {await select(el('target').value);el('error').textContent='';}
|
|
108
|
+
catch(error) {el('error').textContent=error.message;await refresh();}
|
|
109
|
+
};
|
|
110
|
+
el('refresh').onclick=async()=>{
|
|
111
|
+
try {render(await api('/targets',{}));}
|
|
112
|
+
catch(error) {el('error').textContent=error.message;}
|
|
113
|
+
};
|
|
114
|
+
el('compose').onsubmit=async event=>{
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
if(sending||!bindingReady||pendingFault||!state?.target||state.active)return;
|
|
117
|
+
const message=el('draft').value;
|
|
118
|
+
if(!message.replace(/^@ouro(?:\\s+|$)/i,'').trim())return;
|
|
119
|
+
if(pending&&(pending.message!==message||pending.target!==state.target)) {
|
|
120
|
+
el('error').textContent='Reconnect before changing a message whose acknowledgement was lost.';return;
|
|
121
|
+
}
|
|
122
|
+
const submitted=pending||{id:crypto.randomUUID(),target:state.target,message};
|
|
123
|
+
if(new TextEncoder().encode(JSON.stringify(submitted)).byteLength>${exports.MAX_ATTENTION_BODY_BYTES}) {
|
|
124
|
+
el('error').textContent=pending?'The saved request is too large; check its original receipt before changing it.':'Message too large; shorten it. Nothing was sent.';
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
pending=submitted;
|
|
128
|
+
sessionStorage.setItem('ouroAttentionPending',JSON.stringify(pending));
|
|
129
|
+
sending=true;render(state);
|
|
130
|
+
try {const next=await api('/send',submitted);acknowledge(submitted);render(next);}
|
|
131
|
+
catch(error) {
|
|
132
|
+
if(pending?.id===submitted.id) {
|
|
133
|
+
if(error.status===413&&!pendingWasUncertain) {
|
|
134
|
+
pending=null;sessionStorage.removeItem('ouroAttentionPending');
|
|
135
|
+
el('error').textContent=error.message+' Nothing was submitted; shorten the preserved draft.';
|
|
136
|
+
} else {
|
|
137
|
+
pendingWasUncertain=true;
|
|
138
|
+
el('error').textContent=error.message+' Your draft and original request identity are preserved.';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {sending=false;if(state)render(state);}
|
|
143
|
+
};
|
|
144
|
+
el('cancel').onclick=async()=>{
|
|
145
|
+
try {await api('/cancel',{});await refresh();}catch(error) {el('error').textContent=error.message;}
|
|
146
|
+
};
|
|
147
|
+
el('recover').onclick=async()=>{
|
|
148
|
+
try {await api('/recover',{});await refresh();}catch(error) {el('error').textContent=error.message;}
|
|
149
|
+
};
|
|
150
|
+
el('draft').onkeydown=event=>{
|
|
151
|
+
if(event.key==='Enter'&&!event.shiftKey&&!event.isComposing) {event.preventDefault();if(!el('send').disabled)el('compose').requestSubmit();}
|
|
152
|
+
};
|
|
153
|
+
async function initialize() {
|
|
154
|
+
try {
|
|
155
|
+
if(pending&&bindingReady)await refresh();
|
|
156
|
+
else if(initialTarget||!bindingReady)await select(pending?.target||initialTarget||'current');
|
|
157
|
+
else await refresh();
|
|
158
|
+
} catch(error) {bindingReady=false;el('error').textContent=error.message;await refresh();}
|
|
159
|
+
}
|
|
160
|
+
initialize();setInterval(refresh,1000);
|
|
161
|
+
</script></body></html>`;
|
|
162
|
+
}
|
|
163
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.attentionSidebar = attentionSidebar;
|
|
37
|
+
exports.installAttentionSidebar = installAttentionSidebar;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
const MARKER = "// Generated by Ouro session attention.";
|
|
42
|
+
/** Opens beside the coding terminal without moving focus; the view then pins a fresh hint. */
|
|
43
|
+
function attentionSidebar(url) {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1" || !/^[0-9a-f]{64}$/.test(parsed.hash.slice(1))) {
|
|
46
|
+
throw new Error("Attention sidebar requires its private loopback entry.");
|
|
47
|
+
}
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_sidebar_generated", message: "generated the native session attention entry", meta: { format: "swift" } });
|
|
49
|
+
return `${MARKER}
|
|
50
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
51
|
+
Text("Ouro").font(.headline)
|
|
52
|
+
Text("Talk about this session without touching its input.").font(.caption).foregroundColor(.secondary)
|
|
53
|
+
ForEach(workspaces.filter { $0.selected }) { w in
|
|
54
|
+
Button(action: { cmux("browser.open_split", focus: false, workspace_id: w.id, url: ${JSON.stringify(url)}) }) {
|
|
55
|
+
Text("Talk to Ouro")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}.padding(16)
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
function installAttentionSidebar(directory, url, agent = "ouro") {
|
|
62
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
|
|
63
|
+
throw new Error("Attention requires a safe agent name.");
|
|
64
|
+
const file = path.join(directory, `ouro-attention-${agent}.swift`);
|
|
65
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
66
|
+
if (fs.existsSync(file) && !fs.readFileSync(file, "utf8").startsWith(MARKER)) {
|
|
67
|
+
throw new Error("An independently authored ouro-attention sidebar already exists; it was not overwritten.");
|
|
68
|
+
}
|
|
69
|
+
const staging = `${file}.pending`;
|
|
70
|
+
fs.writeFileSync(staging, attentionSidebar(url), { mode: 0o600 });
|
|
71
|
+
fs.chmodSync(staging, 0o600);
|
|
72
|
+
fs.renameSync(staging, file);
|
|
73
|
+
return file;
|
|
74
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.startShepherdAttention = startShepherdAttention;
|
|
37
|
+
exports.startManagedShepherdAttention = startManagedShepherdAttention;
|
|
38
|
+
exports.runShepherdAttention = runShepherdAttention;
|
|
39
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
|
+
const http = __importStar(require("node:http"));
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const os = __importStar(require("node:os"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const frontend_socket_client_1 = require("../../heart/frontend-socket-client");
|
|
45
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
46
|
+
const composer_1 = require("./composer");
|
|
47
|
+
const surface_1 = require("./surface");
|
|
48
|
+
const attention_page_1 = require("./attention-page");
|
|
49
|
+
const attention_sidebar_1 = require("./attention-sidebar");
|
|
50
|
+
function suggestedTarget(targets) {
|
|
51
|
+
const hints = targets.filter(target => target.selected);
|
|
52
|
+
return hints.length === 1 ? hints[0] : targets.length === 1 ? targets[0] : null;
|
|
53
|
+
}
|
|
54
|
+
/** An owner-only loopback view of the existing fixed-target composer, not another model runtime. */
|
|
55
|
+
async function startShepherdAttention(options) {
|
|
56
|
+
const friendId = await (options.resolveOwner ?? (() => (0, surface_1.resolveExistingLocalOwner)((0, surface_1.localShepherdFriendStore)(options.agent))))();
|
|
57
|
+
const makeClient = options.client ?? (() => new frontend_socket_client_1.SocketFrontendClient(options.socketPath));
|
|
58
|
+
const discovery = makeClient();
|
|
59
|
+
let targets;
|
|
60
|
+
try {
|
|
61
|
+
targets = (await discovery.request("shepherd.targets", { agent: options.agent, friendId })).targets;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
discovery.close();
|
|
65
|
+
}
|
|
66
|
+
const selected = options.session
|
|
67
|
+
? (0, composer_1.selectShepherdTarget)(targets, options.session)
|
|
68
|
+
: suggestedTarget(targets);
|
|
69
|
+
const views = new Map();
|
|
70
|
+
const submissions = new Map();
|
|
71
|
+
const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
|
|
72
|
+
const nonce = (0, node_crypto_1.randomBytes)(24).toString("base64");
|
|
73
|
+
let origin = "";
|
|
74
|
+
const append = (view, role, text) => {
|
|
75
|
+
view.messages.push({ role, text });
|
|
76
|
+
view.messages = view.messages.slice(-40);
|
|
77
|
+
};
|
|
78
|
+
const getComposer = (view, target) => {
|
|
79
|
+
if (!view.composer)
|
|
80
|
+
view.composer = new composer_1.ShepherdComposer({
|
|
81
|
+
client: makeClient(), agent: options.agent, friendId, target,
|
|
82
|
+
notice: text => append(view, "notice", text), answer: text => append(view, "ouro", text),
|
|
83
|
+
});
|
|
84
|
+
return view.composer;
|
|
85
|
+
};
|
|
86
|
+
const state = (view, viewId, requestId) => ({
|
|
87
|
+
targets: targets.map(item => ({ id: item.id, label: item.displayLabel ?? "Coding session" })),
|
|
88
|
+
target: view.target?.id ?? null, active: view.composer?.busy ?? false, messages: view.messages,
|
|
89
|
+
acknowledged: requestId && submissions.get(requestId)?.view === viewId
|
|
90
|
+
? { id: requestId, target: submissions.get(requestId).target } : null,
|
|
91
|
+
});
|
|
92
|
+
const server = http.createServer((request, response) => {
|
|
93
|
+
const json = (status, value) => {
|
|
94
|
+
response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
95
|
+
response.end(JSON.stringify(value));
|
|
96
|
+
};
|
|
97
|
+
void (async () => {
|
|
98
|
+
if (request.headers.host !== new URL(origin).host)
|
|
99
|
+
return json(403, { error: "Invalid attention host." });
|
|
100
|
+
if (request.method === "GET" && request.url === "/") {
|
|
101
|
+
response.writeHead(200, {
|
|
102
|
+
"Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store",
|
|
103
|
+
"Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'`,
|
|
104
|
+
"Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
|
|
105
|
+
});
|
|
106
|
+
return response.end((0, attention_page_1.attentionPage)(nonce));
|
|
107
|
+
}
|
|
108
|
+
if (request.headers["x-ouro-attention"] !== token)
|
|
109
|
+
return json(403, { error: "Open attention from its local entry." });
|
|
110
|
+
const viewId = request.headers["x-ouro-view"] ?? "default";
|
|
111
|
+
if (typeof viewId !== "string" || (viewId !== "default" && !surface_1.SHEPHERD_UUID.test(viewId)))
|
|
112
|
+
return json(400, { error: "Invalid attention view." });
|
|
113
|
+
let view = views.get(viewId);
|
|
114
|
+
if (!view) {
|
|
115
|
+
if (views.size >= 64)
|
|
116
|
+
return json(409, { error: "Attention reached its view limit. Restart Shepherd before opening another view." });
|
|
117
|
+
view = { target: selected ?? null, composer: null, messages: [] };
|
|
118
|
+
views.set(viewId, view);
|
|
119
|
+
}
|
|
120
|
+
const pendingId = typeof request.headers["x-ouro-pending"] === "string" ? request.headers["x-ouro-pending"] : undefined;
|
|
121
|
+
if (request.method === "GET" && request.url === "/state")
|
|
122
|
+
return json(200, state(view, viewId, pendingId));
|
|
123
|
+
if (request.method !== "POST" || request.headers.origin !== origin
|
|
124
|
+
|| request.headers["content-type"] !== "application/json")
|
|
125
|
+
return json(403, { error: "Invalid attention origin or content type." });
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
for await (const chunk of request) {
|
|
129
|
+
const buffer = Buffer.from(chunk);
|
|
130
|
+
bytes += buffer.length;
|
|
131
|
+
if (bytes > attention_page_1.MAX_ATTENTION_BODY_BYTES)
|
|
132
|
+
return json(413, { error: "Message is too large." });
|
|
133
|
+
chunks.push(buffer);
|
|
134
|
+
}
|
|
135
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
136
|
+
let input;
|
|
137
|
+
try {
|
|
138
|
+
input = JSON.parse(body);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return json(400, { error: "Invalid JSON." });
|
|
142
|
+
}
|
|
143
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
144
|
+
return json(400, { error: "Invalid request." });
|
|
145
|
+
const fields = input;
|
|
146
|
+
if (request.url === "/targets") {
|
|
147
|
+
const refresh = makeClient();
|
|
148
|
+
try {
|
|
149
|
+
targets = (await refresh.request("shepherd.targets", { agent: options.agent, friendId })).targets;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
refresh.close();
|
|
153
|
+
}
|
|
154
|
+
return json(200, state(view, viewId, pendingId));
|
|
155
|
+
}
|
|
156
|
+
if (request.url === "/select") {
|
|
157
|
+
if (view.composer?.busy)
|
|
158
|
+
return json(409, { error: "Wait for or cancel Ouro before changing the session." });
|
|
159
|
+
if (typeof fields.target !== "string")
|
|
160
|
+
return json(400, { error: "Choose a session." });
|
|
161
|
+
const refresh = makeClient();
|
|
162
|
+
try {
|
|
163
|
+
targets = (await refresh.request("shepherd.targets", { agent: options.agent, friendId })).targets;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
refresh.close();
|
|
167
|
+
}
|
|
168
|
+
if (view.composer?.busy)
|
|
169
|
+
return json(409, { error: "Ouro started replying while the session list refreshed; the conversation was not changed." });
|
|
170
|
+
const next = fields.target === "current" ? suggestedTarget(targets) : (0, composer_1.selectShepherdTarget)(targets, fields.target);
|
|
171
|
+
if (view.target?.id !== next?.id) {
|
|
172
|
+
view.composer?.close();
|
|
173
|
+
view.composer = null;
|
|
174
|
+
view.target = next;
|
|
175
|
+
view.messages = [];
|
|
176
|
+
}
|
|
177
|
+
return json(200, state(view, viewId, pendingId));
|
|
178
|
+
}
|
|
179
|
+
if (request.url === "/send") {
|
|
180
|
+
if (typeof fields.id !== "string" || !surface_1.SHEPHERD_UUID.test(fields.id)
|
|
181
|
+
|| typeof fields.message !== "string" || !fields.message.replace(/^@ouro(?:\s+|$)/i, "").trim())
|
|
182
|
+
return json(400, { error: "Enter a message for Ouro." });
|
|
183
|
+
if (!view.target)
|
|
184
|
+
return json(409, { error: "Choose the session you want to discuss." });
|
|
185
|
+
if (fields.target !== undefined && fields.target !== view.target.id)
|
|
186
|
+
return json(409, { error: "This pending message is pinned to another session." });
|
|
187
|
+
const prior = submissions.get(fields.id);
|
|
188
|
+
if (prior) {
|
|
189
|
+
if (prior.view !== viewId || prior.target !== view.target.id || prior.message !== fields.message)
|
|
190
|
+
return json(409, { error: "This request id belongs to a different message or session." });
|
|
191
|
+
return json(202, state(view, viewId, fields.id));
|
|
192
|
+
}
|
|
193
|
+
if (view.composer?.busy)
|
|
194
|
+
return json(409, { error: "Ouro is already replying. Your draft has not been sent." });
|
|
195
|
+
if (submissions.size >= 2_000)
|
|
196
|
+
return json(409, { error: "Attention reached its message limit. Restart Shepherd before sending more messages." });
|
|
197
|
+
const bound = getComposer(view, view.target);
|
|
198
|
+
submissions.set(fields.id, { view: viewId, target: view.target.id, message: fields.message });
|
|
199
|
+
append(view, "you", fields.message);
|
|
200
|
+
const submittedView = view;
|
|
201
|
+
void bound.submit(fields.message, fields.id).catch(error => append(submittedView, "notice", String(error)));
|
|
202
|
+
return json(202, state(view, viewId, fields.id));
|
|
203
|
+
}
|
|
204
|
+
if (request.url === "/cancel") {
|
|
205
|
+
const cancelled = await view.composer?.cancel() ?? false;
|
|
206
|
+
return json(200, { cancelled });
|
|
207
|
+
}
|
|
208
|
+
if (request.url === "/recover") {
|
|
209
|
+
await view.composer?.recover();
|
|
210
|
+
return json(200, state(view, viewId, pendingId));
|
|
211
|
+
}
|
|
212
|
+
return json(404, { error: "Unknown attention action." });
|
|
213
|
+
})().catch(error => {
|
|
214
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_attention_error", message: "attention action failed", meta: { error: String(error) } });
|
|
215
|
+
if (!response.headersSent)
|
|
216
|
+
json(400, { error: String(error) });
|
|
217
|
+
else
|
|
218
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
server.once("error", reject);
|
|
223
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
224
|
+
});
|
|
225
|
+
const address = server.address();
|
|
226
|
+
if (!address || typeof address === "string") {
|
|
227
|
+
server.close();
|
|
228
|
+
throw new Error("Attention did not bind a loopback port.");
|
|
229
|
+
}
|
|
230
|
+
origin = `http://127.0.0.1:${address.port}`;
|
|
231
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_attention_started", message: "opened independent graphical session attention", meta: { agent: options.agent } });
|
|
232
|
+
return {
|
|
233
|
+
url: `${origin}/#${token}`,
|
|
234
|
+
async close() {
|
|
235
|
+
try {
|
|
236
|
+
await Promise.all([...views.values()].map(view => view.composer?.cancel()));
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
for (const view of views.values())
|
|
240
|
+
view.composer?.close();
|
|
241
|
+
server.closeAllConnections();
|
|
242
|
+
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function startManagedShepherdAttention(options) {
|
|
248
|
+
const app = await startShepherdAttention(options);
|
|
249
|
+
try {
|
|
250
|
+
(0, attention_sidebar_1.installAttentionSidebar)(path.join(os.homedir(), ".config/cmux/sidebars"), app.url, options.agent);
|
|
251
|
+
const validated = (0, node_child_process_1.spawnSync)("cmux", ["sidebar", "validate", `ouro-attention-${options.agent}`], { encoding: "utf8" });
|
|
252
|
+
if (validated.error || validated.status !== 0)
|
|
253
|
+
throw new Error(`Could not validate Ouro's native entry: ${validated.error?.message ?? validated.stderr}`);
|
|
254
|
+
return app;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
await app.close();
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function runShepherdAttention(options) {
|
|
262
|
+
const app = await startShepherdAttention(options);
|
|
263
|
+
try {
|
|
264
|
+
const opened = (0, node_child_process_1.spawnSync)("cmux", ["browser", "open", app.url + (options.session ? `&session=${options.session}` : ""), "--focus", "false"], { encoding: "utf8" });
|
|
265
|
+
if (opened.error || opened.status !== 0)
|
|
266
|
+
throw new Error(`Could not open Ouro attention in cmux: ${opened.error?.message ?? opened.stderr}`);
|
|
267
|
+
await new Promise(resolve => {
|
|
268
|
+
const stop = () => {
|
|
269
|
+
process.removeListener("SIGINT", stop);
|
|
270
|
+
process.removeListener("SIGTERM", stop);
|
|
271
|
+
resolve();
|
|
272
|
+
};
|
|
273
|
+
process.once("SIGINT", stop);
|
|
274
|
+
process.once("SIGTERM", stop);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
await app.close();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -142,6 +142,8 @@ function createCmuxHost(client, options = {}) {
|
|
|
142
142
|
cwd: hooked.get(str(surface.id) ?? "")?.cwd ?? null,
|
|
143
143
|
// The human's terminal: the focused surface of the selected workspace in a visible window.
|
|
144
144
|
focused: surface.focused === true && workspace.selected === true && window.visible !== false,
|
|
145
|
+
...(surface.focused === true && workspace.selected === true && window.visible !== false && window.key === true
|
|
146
|
+
? { selected: true } : {}),
|
|
145
147
|
})));
|
|
146
148
|
const identities = await (options.identifyTerminals ?? terminal_process_1.identifyTerminalProcesses)(entries.filter((entry) => entry.type === "terminal" && entry.tty).map((entry) => entry.tty));
|
|
147
149
|
return entries.map((entry) => {
|
|
@@ -77,6 +77,7 @@ class ShepherdComposer {
|
|
|
77
77
|
unsubscribe;
|
|
78
78
|
unsubscribeClose;
|
|
79
79
|
notice;
|
|
80
|
+
answer;
|
|
80
81
|
constructor(options) {
|
|
81
82
|
this.client = options.client;
|
|
82
83
|
this.agent = options.agent;
|
|
@@ -85,6 +86,7 @@ class ShepherdComposer {
|
|
|
85
86
|
this.target = Object.freeze({ ...options.target });
|
|
86
87
|
this.sessionKey = `shepherd:${this.target.id}`;
|
|
87
88
|
this.notice = options.notice ?? (() => { });
|
|
89
|
+
this.answer = options.answer ?? (() => { });
|
|
88
90
|
this.unsubscribe = this.client.onEvent(frame => {
|
|
89
91
|
if (frame.event === "replay_required") {
|
|
90
92
|
const turn = this.active;
|
|
@@ -103,14 +105,17 @@ class ShepherdComposer {
|
|
|
103
105
|
this.fail(error, true); });
|
|
104
106
|
});
|
|
105
107
|
}
|
|
106
|
-
|
|
108
|
+
get busy() { return this.active !== null; }
|
|
109
|
+
async submit(input, requestId) {
|
|
107
110
|
if (this.active)
|
|
108
111
|
throw new Error("Ouro already has an active turn; use /cancel");
|
|
112
|
+
if (requestId !== undefined && !surface_1.SHEPHERD_UUID.test(requestId))
|
|
113
|
+
throw new Error("Ouro request id must be a UUID");
|
|
109
114
|
const message = input.replace(/^@ouro(?:\s+|$)/i, "");
|
|
110
115
|
if (!message.trim())
|
|
111
116
|
throw new Error("message must not be empty");
|
|
112
117
|
const completion = Promise.withResolvers();
|
|
113
|
-
const turn = { id: (0, node_crypto_1.randomUUID)(), events: [], seen: new Set(), recovering: 0, reconciliationPending: false, dispatched: false, cancelled: false,
|
|
118
|
+
const turn = { id: requestId ?? (0, node_crypto_1.randomUUID)(), events: [], seen: new Set(), recovering: 0, reconciliationPending: false, dispatched: false, cancelled: false,
|
|
114
119
|
unresolved: false, resolve: completion.resolve, reject: completion.reject };
|
|
115
120
|
this.active = turn;
|
|
116
121
|
// Attach a rejection handler before transport work to avoid unhandled rejection races.
|
|
@@ -128,6 +133,11 @@ class ShepherdComposer {
|
|
|
128
133
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_composer_turn_submitted", message: "submitted an independent fixed-target Ouro turn", meta: { agent: this.agent, session: this.target.id, turnId } });
|
|
129
134
|
}
|
|
130
135
|
catch (sendError) {
|
|
136
|
+
if (sendError instanceof frontend_socket_client_1.FrontendRequestError && sendError.code === "turn_rejected") {
|
|
137
|
+
if (this.active === turn)
|
|
138
|
+
this.fail(sendError);
|
|
139
|
+
return completion.promise;
|
|
140
|
+
}
|
|
131
141
|
// Unknown acknowledgement is never retried. Even a rejected send may have run.
|
|
132
142
|
if (this.active === turn) {
|
|
133
143
|
try {
|
|
@@ -230,6 +240,7 @@ class ShepherdComposer {
|
|
|
230
240
|
}
|
|
231
241
|
else {
|
|
232
242
|
const answer = deliveries.length ? deliveries.join("\n") : String(data.result?.response ?? "");
|
|
243
|
+
this.answer(answer);
|
|
233
244
|
if (turn.unresolved)
|
|
234
245
|
this.notice(`Ouro: ${answer}`);
|
|
235
246
|
turn.resolve(answer);
|
|
@@ -99,6 +99,10 @@ async function listShepherdTargets(host, composerTty) {
|
|
|
99
99
|
host: "cmux",
|
|
100
100
|
label: [session.agent, session.cwd, session.workspaceTitle, session.title]
|
|
101
101
|
.filter(Boolean).map(part => part.replace(/[\x00-\x1f\x7f-\x9f]/g, "").slice(0, 160)).join(" / "),
|
|
102
|
+
displayLabel: [session.title, session.workspaceTitle, `${session.agent} session`, "Coding session"]
|
|
103
|
+
.map(part => (part ?? "").replace(/[\x00-\x1f\x7f-\x9f]/g, "").trim())
|
|
104
|
+
.find(part => part && !/[\\/]/.test(part) && !/[0-9a-f]{8}-[0-9a-f-]{27,}/i.test(part)).slice(0, 72),
|
|
105
|
+
selected: session.selected === true,
|
|
102
106
|
}));
|
|
103
107
|
}
|
|
104
108
|
async function validateShepherdTarget(host, id) {
|
|
@@ -57,9 +57,29 @@ Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"
|
|
|
57
57
|
}
|
|
58
58
|
const { startShepherdSenseApp } = await Promise.resolve().then(() => __importStar(require("./shepherd/sense")));
|
|
59
59
|
const app = await startShepherdSenseApp({ agentName: agentName });
|
|
60
|
+
let attention;
|
|
61
|
+
if (process.platform === "darwin") {
|
|
62
|
+
const { resolveShepherdConnection } = await Promise.resolve().then(() => __importStar(require("../heart/shepherd-config")));
|
|
63
|
+
const machine = readMachineRuntimeCredentialConfig(agentName);
|
|
64
|
+
const connection = machine.ok ? resolveShepherdConnection(agentName, machine.config) : null;
|
|
65
|
+
if (connection?.ok && connection.connection.host === "cmux") {
|
|
66
|
+
const { startManagedShepherdAttention } = await Promise.resolve().then(() => __importStar(require("./shepherd/attention")));
|
|
67
|
+
const { DEFAULT_DAEMON_SOCKET_PATH } = await Promise.resolve().then(() => __importStar(require("../heart/daemon/socket-client")));
|
|
68
|
+
const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../heart/frontend-socket")));
|
|
69
|
+
try {
|
|
70
|
+
attention = await startManagedShepherdAttention({ agent: agentName, socketPath: frontendSocketPathForDaemon(DEFAULT_DAEMON_SOCKET_PATH) });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_attention_unavailable", message: "native session attention is unavailable; Shepherd supervision remains running", meta: { agentName, error: String(error) } });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
const shutdown = () => {
|
|
61
78
|
app.stop();
|
|
62
|
-
process.exit(0)
|
|
79
|
+
void (attention?.close() ?? Promise.resolve()).then(() => process.exit(0), error => {
|
|
80
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.shepherd_attention_shutdown_error", message: "could not close native session attention", meta: { error: String(error) } });
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
63
83
|
};
|
|
64
84
|
process.once("SIGTERM", shutdown);
|
|
65
85
|
process.once("SIGINT", shutdown);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.926",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.926",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|