@ouro.bot/cli 0.1.0-alpha.959 → 0.1.0-alpha.960
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 +6 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/shepherd/operations.js +1 -1
- package/dist/heart/shepherd-config.js +13 -3
- package/dist/senses/shepherd/copilot-app.js +648 -0
- package/dist/senses/shepherd/host.js +3 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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.960",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Shepherd can watch sessions in the GitHub Copilot app. Add a `copilotApp` record to the machine config (`ouro vault config set --agent <agent> --scope machine --key copilotApp.stateDir --value \"$HOME/.copilot\"`) and Shepherd follows the app's sessions instead of cmux or Herdr: it notices when one stops or waits for a permission, judges it, and answers with a queued message. It leaves alone a session you messaged in the last five minutes, or in the last half hour while you are at the Mac with the app in front, and a turn you stopped yourself. Messages from other agents, schedules and Shepherd itself do not count as yours."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.959",
|
|
6
12
|
"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.960</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -87,7 +87,7 @@ function parseShepherdOperation(args) {
|
|
|
87
87
|
const common = { agent, ...(values.has("json") ? { json: true } : {}) };
|
|
88
88
|
const host = () => {
|
|
89
89
|
const value = required("host");
|
|
90
|
-
return value === "cmux" || value === "herdr" ? value : fail();
|
|
90
|
+
return value === "cmux" || value === "herdr" || value === "copilot-app" ? value : fail();
|
|
91
91
|
};
|
|
92
92
|
if (operation === "status")
|
|
93
93
|
return { kind: "shepherd.status", ...common };
|
|
@@ -37,6 +37,7 @@ exports.shepherdRepairHint = shepherdRepairHint;
|
|
|
37
37
|
exports.shepherdConfigFacts = shepherdConfigFacts;
|
|
38
38
|
exports.shepherdSenseStatus = shepherdSenseStatus;
|
|
39
39
|
exports.defaultHerdrSocketPath = defaultHerdrSocketPath;
|
|
40
|
+
exports.defaultCopilotAppStateDir = defaultCopilotAppStateDir;
|
|
40
41
|
exports.defaultCmuxSocketPath = defaultCmuxSocketPath;
|
|
41
42
|
exports.resolveShepherdConnection = resolveShepherdConnection;
|
|
42
43
|
const fs = __importStar(require("node:fs"));
|
|
@@ -44,7 +45,7 @@ const os = __importStar(require("node:os"));
|
|
|
44
45
|
const path = __importStar(require("node:path"));
|
|
45
46
|
const runtime_1 = require("../nerves/runtime");
|
|
46
47
|
function shepherdRepairHint(agent) {
|
|
47
|
-
return `human-required: in a cmux terminal run 'ouro vault config set --agent ${agent} --scope machine --key cmux.socketCapability --value "$CMUX_SOCKET_CAPABILITY"' (or set cmux.socketPassword when cmux is in password mode), then 'ouro up'. On Herdr instead: 'ouro vault config set --agent ${agent} --scope machine --key herdr.socketPath --value "$HOME/.config/herdr/herdr.sock"', then 'ouro up'.`;
|
|
48
|
+
return `human-required: for the GitHub Copilot app run 'ouro vault config set --agent ${agent} --scope machine --key copilotApp.stateDir --value "$HOME/.copilot"', then 'ouro up'. Or in a cmux terminal run 'ouro vault config set --agent ${agent} --scope machine --key cmux.socketCapability --value "$CMUX_SOCKET_CAPABILITY"' (or set cmux.socketPassword when cmux is in password mode), then 'ouro up'. On Herdr instead: 'ouro vault config set --agent ${agent} --scope machine --key herdr.socketPath --value "$HOME/.config/herdr/herdr.sock"', then 'ouro up'.`;
|
|
48
49
|
}
|
|
49
50
|
function text(record, key) {
|
|
50
51
|
const value = record?.[key];
|
|
@@ -66,6 +67,8 @@ function cmuxAuth(cmux) {
|
|
|
66
67
|
return null;
|
|
67
68
|
}
|
|
68
69
|
function shepherdConfigFacts(machinePayload) {
|
|
70
|
+
if (hostRecord(machinePayload, "copilotApp"))
|
|
71
|
+
return { configured: true, detail: "GitHub Copilot app" };
|
|
69
72
|
const cmux = hostRecord(machinePayload, "cmux");
|
|
70
73
|
const auth = cmuxAuth(cmux);
|
|
71
74
|
if (!auth && hostRecord(machinePayload, "herdr"))
|
|
@@ -87,6 +90,10 @@ function shepherdSenseStatus(enabled, machinePayload) {
|
|
|
87
90
|
function defaultHerdrSocketPath(homeDir = os.homedir()) {
|
|
88
91
|
return path.join(homeDir, ".config", "herdr", "herdr.sock");
|
|
89
92
|
}
|
|
93
|
+
/** The Copilot app's state directory, where it writes `run/ws.port` and `run/ws.token`. */
|
|
94
|
+
function defaultCopilotAppStateDir(homeDir = os.homedir()) {
|
|
95
|
+
return path.join(homeDir, ".copilot");
|
|
96
|
+
}
|
|
90
97
|
/** cmux's own default: `~/.local/state/cmux/cmux-<uid>.sock`, then the older unscoped `cmux.sock`. */
|
|
91
98
|
function defaultCmuxSocketPath(homeDir = os.homedir(), uid = process.getuid?.() ?? 0, exists = fs.existsSync) {
|
|
92
99
|
const dir = path.join(homeDir, ".local", "state", "cmux");
|
|
@@ -97,14 +104,17 @@ function defaultCmuxSocketPath(homeDir = os.homedir(), uid = process.getuid?.()
|
|
|
97
104
|
function resolveShepherdConnection(agent, machinePayload, defaultSocketPath = defaultCmuxSocketPath) {
|
|
98
105
|
const cmux = hostRecord(machinePayload, "cmux");
|
|
99
106
|
const herdr = hostRecord(machinePayload, "herdr");
|
|
107
|
+
const copilotApp = hostRecord(machinePayload, "copilotApp");
|
|
100
108
|
const auth = cmuxAuth(cmux);
|
|
101
|
-
const host = auth ? "cmux" : herdr ? "herdr" : "none";
|
|
109
|
+
const host = copilotApp ? "copilot-app" : auth ? "cmux" : herdr ? "herdr" : "none";
|
|
102
110
|
(0, runtime_1.emitNervesEvent)({
|
|
103
111
|
component: "senses",
|
|
104
112
|
event: "senses.shepherd_connection_resolved",
|
|
105
113
|
message: "resolved Shepherd terminal host connection settings",
|
|
106
|
-
meta: { agent, host, auth: auth?.kind ?? "missing" },
|
|
114
|
+
meta: { agent, host, auth: host === "copilot-app" ? "app-token-file" : auth?.kind ?? "missing" },
|
|
107
115
|
});
|
|
116
|
+
if (copilotApp)
|
|
117
|
+
return { ok: true, connection: { host: "copilot-app", stateDir: text(copilotApp, "stateDir") || defaultCopilotAppStateDir() } };
|
|
108
118
|
if (auth)
|
|
109
119
|
return { ok: true, connection: { host: "cmux", socketPath: text(cmux, "socketPath") || defaultSocketPath(), auth } };
|
|
110
120
|
if (herdr)
|
|
@@ -0,0 +1,648 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.COPILOT_APP_HOST = void 0;
|
|
40
|
+
exports.copilotAppUrl = copilotAppUrl;
|
|
41
|
+
exports.copilotAppHumanPresent = copilotAppHumanPresent;
|
|
42
|
+
exports.readConversation = readConversation;
|
|
43
|
+
exports.copilotAppRequest = copilotAppRequest;
|
|
44
|
+
exports.createCopilotAppHost = createCopilotAppHost;
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const node_crypto_1 = require("node:crypto");
|
|
49
|
+
const ws_1 = __importDefault(require("ws"));
|
|
50
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
51
|
+
const cmux_1 = require("./cmux");
|
|
52
|
+
/**
|
|
53
|
+
* The GitHub Copilot app host. The app runs a local WebSocket server whose port and token it
|
|
54
|
+
* rewrites under `<stateDir>/run/` on every launch, so each connection reads them fresh. Shepherd
|
|
55
|
+
* connects as an observer (`observer=1`), so the app never binds a session's tool callbacks to it.
|
|
56
|
+
*
|
|
57
|
+
* Every connection receives every running session's events. A return is `session.idle` (not a
|
|
58
|
+
* human stop), a permission or question the human must answer, or a terminal `session.error`, still
|
|
59
|
+
* standing after the grace period. `assistant.turn_end` is not a return: the app sends one after
|
|
60
|
+
* every model step. Sub-agent events (top-level `agentId`) are work, never a return. The "screen" is
|
|
61
|
+
* the session's recent conversation, rendered from `session-state/<id>/events.jsonl`. A reply is a
|
|
62
|
+
* queued `send_message`, so it never touches the human's draft.
|
|
63
|
+
*
|
|
64
|
+
* The app does not say which session is on screen, so a session counts as the human's when the
|
|
65
|
+
* human messaged it in the last five minutes, or in the last half hour while they are at the Mac with
|
|
66
|
+
* the app in front. Only a message with no `source` and no `agentId` is the human's: agents,
|
|
67
|
+
* schedules and autopilot tag theirs, and Shepherd's carry its `[agent for human]` label.
|
|
68
|
+
*/
|
|
69
|
+
exports.COPILOT_APP_HOST = "copilot-app";
|
|
70
|
+
const GRACE_MS = 3_000;
|
|
71
|
+
const TIMEOUT_MS = 10_000;
|
|
72
|
+
const HUMAN_HOLD_MS = 5 * 60_000;
|
|
73
|
+
const PRESENT_HOLD_MS = 30 * 60_000;
|
|
74
|
+
const PRESENT_IDLE_S = 120;
|
|
75
|
+
const INVENTORY_MS = 10_000;
|
|
76
|
+
const CHUNK_BYTES = 256 * 1024;
|
|
77
|
+
const SCAN_BYTES = 8 * 1024 * 1024;
|
|
78
|
+
const TAIL_EVENTS = 400;
|
|
79
|
+
const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
|
|
80
|
+
/** Bookkeeping events that neither start nor end work. */
|
|
81
|
+
const QUIET_EVENTS = new Set(["model.messages_snapshot", "session.usage_checkpoint", "session.usage_record", "session.background_tasks_changed", "session.title_changed"]);
|
|
82
|
+
const BLOCKED_FRAMES = new Set(["user_input_request", "exit_plan_mode_request", "elicitation_request", "auto_mode_switch_request"]);
|
|
83
|
+
const RESOLVED_FRAMES = new Set(["user_input_resolved", "exit_plan_mode_resolved", "elicitation_resolved", "auto_mode_switch_resolved"]);
|
|
84
|
+
/** Every Shepherd message starts with its `[agent for human]` label, whichever process sent it. */
|
|
85
|
+
const SHEPHERD_LABEL = /^\[[^\]\n]{1,80} for [^\]\n]{1,80}\] /;
|
|
86
|
+
const PERMISSION_KEYS = { y: "approve-once", 1: "approve-once", 2: "approve-for-session", n: "reject", 3: "reject" };
|
|
87
|
+
/** Asset lines can be megabytes and carry no conversation. */
|
|
88
|
+
const BINARY_LINE = Buffer.from("\"session.binary_asset\"");
|
|
89
|
+
function isRecord(value) {
|
|
90
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
91
|
+
}
|
|
92
|
+
function str(value) {
|
|
93
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
94
|
+
}
|
|
95
|
+
function processAlive(pid) {
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 0);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return error.code === "EPERM";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The app's current server address. Every app build (release, dev, PR) writes `ws.port` and
|
|
106
|
+
* `ws.token`; the release app also writes `ws.release.*`, so those win. The port file holds
|
|
107
|
+
* `<port>\n<pid>`, and a file whose app has exited is skipped.
|
|
108
|
+
*/
|
|
109
|
+
function copilotAppUrl(stateDir, alive = processAlive) {
|
|
110
|
+
const run = path.join(stateDir, "run");
|
|
111
|
+
for (const prefix of ["ws.release", "ws"]) {
|
|
112
|
+
let port, pid, token;
|
|
113
|
+
try {
|
|
114
|
+
;
|
|
115
|
+
[port, pid] = fs.readFileSync(path.join(run, `${prefix}.port`), "utf-8").split(/\r?\n/).map(line => line.trim());
|
|
116
|
+
token = fs.readFileSync(path.join(run, `${prefix}.token`), "utf-8").split(/\r?\n/)[0].trim();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!/^\d+$/.test(port) || !token)
|
|
122
|
+
continue;
|
|
123
|
+
if (pid && /^\d+$/.test(pid) && !alive(Number(pid)))
|
|
124
|
+
continue;
|
|
125
|
+
return `ws://127.0.0.1:${port}?token=${encodeURIComponent(token)}&observer=1`;
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`the GitHub Copilot app is not running (no live server files in ${run})`);
|
|
128
|
+
}
|
|
129
|
+
const runFile = (file, args, done) => { (0, node_child_process_1.execFile)(file, args, { timeout: 2_000 }, (error, stdout) => done(error, String(stdout))); };
|
|
130
|
+
/** macOS: the Copilot app is frontmost and the keyboard or mouse moved recently. Anything unreadable counts as present, so Shepherd holds back. */
|
|
131
|
+
function copilotAppHumanPresent(run = runFile) {
|
|
132
|
+
const output = (file, args) => new Promise(resolve => run(file, args, (error, stdout) => resolve(error ? null : stdout)));
|
|
133
|
+
return (async () => {
|
|
134
|
+
const asn = (await output("/usr/bin/lsappinfo", ["front"]))?.trim();
|
|
135
|
+
if (!asn)
|
|
136
|
+
return true;
|
|
137
|
+
const info = await output("/usr/bin/lsappinfo", ["info", "-only", "name", asn]);
|
|
138
|
+
if (info === null)
|
|
139
|
+
return true;
|
|
140
|
+
if (!/"GitHub Copilot"/.test(info))
|
|
141
|
+
return false;
|
|
142
|
+
const hid = await output("/usr/sbin/ioreg", ["-c", "IOHIDSystem", "-d", "4"]);
|
|
143
|
+
const idle = hid?.match(/"HIDIdleTime" = (\d+)/);
|
|
144
|
+
return !idle || Number(idle[1]) / 1e9 < PRESENT_IDLE_S;
|
|
145
|
+
})();
|
|
146
|
+
}
|
|
147
|
+
/** Whether a `user.message` is the human's own: agents, schedules and autopilot tag theirs. */
|
|
148
|
+
function humanMessage(event, data) {
|
|
149
|
+
return event.type === "user.message" && event.agentId === undefined && data.source === undefined && !SHEPHERD_LABEL.test(str(data.content) ?? "");
|
|
150
|
+
}
|
|
151
|
+
/** Up to `TAIL_EVENTS` event lines from the end of the log, oldest first, skipping asset lines. */
|
|
152
|
+
function tailEvents(file) {
|
|
153
|
+
const fd = fs.openSync(file, "r");
|
|
154
|
+
const lines = [];
|
|
155
|
+
try {
|
|
156
|
+
let position = fs.fstatSync(fd).size;
|
|
157
|
+
let carry = Buffer.alloc(0);
|
|
158
|
+
const keep = (line) => {
|
|
159
|
+
if (line.length > 0 && !line.subarray(0, 200).includes(BINARY_LINE))
|
|
160
|
+
lines.push(line);
|
|
161
|
+
};
|
|
162
|
+
while (position > 0 && lines.length < TAIL_EVENTS) {
|
|
163
|
+
const length = Math.min(CHUNK_BYTES, position);
|
|
164
|
+
position -= length;
|
|
165
|
+
const chunk = Buffer.alloc(length);
|
|
166
|
+
fs.readSync(fd, chunk, 0, length, position);
|
|
167
|
+
const text = Buffer.concat([chunk, carry]);
|
|
168
|
+
let end = text.length;
|
|
169
|
+
for (let at = text.lastIndexOf(10, end - 1); at >= 0; at = text.lastIndexOf(10, end - 1)) {
|
|
170
|
+
keep(text.subarray(at + 1, end));
|
|
171
|
+
end = at;
|
|
172
|
+
if (lines.length >= TAIL_EVENTS)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
carry = text.subarray(0, end);
|
|
176
|
+
// One huge line (an asset) is dropped once it outgrows the scan budget.
|
|
177
|
+
if (carry.length > SCAN_BYTES)
|
|
178
|
+
carry = Buffer.alloc(0);
|
|
179
|
+
}
|
|
180
|
+
if (position === 0 && lines.length < TAIL_EVENTS)
|
|
181
|
+
keep(carry);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
fs.closeSync(fd);
|
|
185
|
+
}
|
|
186
|
+
return lines.reverse();
|
|
187
|
+
}
|
|
188
|
+
/** The session's recent top-level conversation, oldest first, and what is still waiting on the human. */
|
|
189
|
+
function readConversation(stateDir, sessionId) {
|
|
190
|
+
if (!/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
191
|
+
throw new Error("invalid Copilot app session id");
|
|
192
|
+
const lines = [];
|
|
193
|
+
const permissions = new Map();
|
|
194
|
+
let humanAt = null;
|
|
195
|
+
const push = (block) => {
|
|
196
|
+
for (const line of block.split(/\r?\n/))
|
|
197
|
+
lines.push(line);
|
|
198
|
+
};
|
|
199
|
+
for (const raw of tailEvents(path.join(stateDir, "session-state", sessionId, "events.jsonl"))) {
|
|
200
|
+
let event;
|
|
201
|
+
try {
|
|
202
|
+
event = JSON.parse(raw.toString("utf-8"));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!isRecord(event))
|
|
208
|
+
continue;
|
|
209
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
210
|
+
if (humanMessage(event, data)) {
|
|
211
|
+
const at = Date.parse(str(event.timestamp) ?? "");
|
|
212
|
+
if (!Number.isNaN(at))
|
|
213
|
+
humanAt = at;
|
|
214
|
+
}
|
|
215
|
+
// Sub-agents talk among themselves; the conversation is the session's own.
|
|
216
|
+
if (event.agentId !== undefined)
|
|
217
|
+
continue;
|
|
218
|
+
switch (event.type) {
|
|
219
|
+
case "user.message":
|
|
220
|
+
if (str(data.content))
|
|
221
|
+
push(`› ${str(data.source) ? `(${data.source}) ` : ""}${data.content}`);
|
|
222
|
+
break;
|
|
223
|
+
case "assistant.message":
|
|
224
|
+
if (str(data.content))
|
|
225
|
+
push(data.content);
|
|
226
|
+
break;
|
|
227
|
+
case "tool.execution_start": {
|
|
228
|
+
const args = isRecord(data.arguments) ? data.arguments : {};
|
|
229
|
+
const what = str(args.description) ?? str(args.command) ?? str(args.question) ?? str(args.path) ?? "";
|
|
230
|
+
push(`• ${str(data.toolName) ?? "tool"}${what ? `: ${what.replace(/\s+/g, " ").slice(0, 160)}` : ""}`);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case "session.task_complete":
|
|
234
|
+
if (str(data.summary))
|
|
235
|
+
push(`Task complete: ${data.summary}`);
|
|
236
|
+
break;
|
|
237
|
+
case "session.error":
|
|
238
|
+
push(`Error: ${str(data.message) ?? str(data.errorType) ?? "unknown"}`);
|
|
239
|
+
break;
|
|
240
|
+
case "permission.requested": {
|
|
241
|
+
const id = str(data.requestId);
|
|
242
|
+
if (!id || data.resolvedByHook === true)
|
|
243
|
+
break;
|
|
244
|
+
const request = isRecord(data.permissionRequest) ? data.permissionRequest : {};
|
|
245
|
+
permissions.set(id, str(request.intention) ?? str(request.fullCommandText) ?? str(request.kind) ?? "a tool call");
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "permission.completed":
|
|
249
|
+
if (str(data.requestId))
|
|
250
|
+
permissions.delete(data.requestId);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const pending = [...permissions.entries()].at(-1);
|
|
255
|
+
if (pending)
|
|
256
|
+
lines.push(`Waiting for permission: ${pending[1].replace(/\s+/g, " ").slice(0, 300)}`);
|
|
257
|
+
return { lines, pendingPermission: pending?.[0] ?? null, humanAt };
|
|
258
|
+
}
|
|
259
|
+
/** One connection: frames go to `onFrame` until the socket closes. Throws if the socket cannot be created. */
|
|
260
|
+
function openStream(url, openSocket, onFrame, onClose) {
|
|
261
|
+
const socket = openSocket(url);
|
|
262
|
+
const queue = [];
|
|
263
|
+
let failure;
|
|
264
|
+
socket.on("open", () => { for (const line of queue.splice(0))
|
|
265
|
+
socket.send(line); });
|
|
266
|
+
socket.on("message", (data) => {
|
|
267
|
+
try {
|
|
268
|
+
const frame = JSON.parse(data.toString());
|
|
269
|
+
if (isRecord(frame))
|
|
270
|
+
onFrame(frame);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// A frame that is not JSON is ignored.
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
socket.on("error", (error) => { failure = error; });
|
|
277
|
+
socket.on("close", () => onClose(failure));
|
|
278
|
+
return {
|
|
279
|
+
send(frame) {
|
|
280
|
+
const line = JSON.stringify(frame);
|
|
281
|
+
if (socket.readyState === ws_1.default.OPEN)
|
|
282
|
+
socket.send(line);
|
|
283
|
+
else
|
|
284
|
+
queue.push(line);
|
|
285
|
+
},
|
|
286
|
+
close: () => socket.close(),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** One request on its own connection, settled by the first frame `match` accepts (or throws on). */
|
|
290
|
+
function copilotAppRequest(stateDir, frame, match, options = {}) {
|
|
291
|
+
const openSocket = options.openSocket ?? ((url) => new ws_1.default(url));
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
let stream;
|
|
294
|
+
try {
|
|
295
|
+
stream = openStream(copilotAppUrl(stateDir), openSocket, (reply) => {
|
|
296
|
+
try {
|
|
297
|
+
const value = match(reply);
|
|
298
|
+
if (value !== undefined)
|
|
299
|
+
settle(() => resolve(value));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
settle(() => reject(error));
|
|
303
|
+
}
|
|
304
|
+
}, (error) => settle(() => reject(new Error(`Copilot app ${String(frame.type)}: ${error?.message ?? "connection closed"}`))));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return reject(error);
|
|
308
|
+
}
|
|
309
|
+
let settled = false;
|
|
310
|
+
const settle = (fn) => {
|
|
311
|
+
if (settled)
|
|
312
|
+
return;
|
|
313
|
+
settled = true;
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
stream.close();
|
|
316
|
+
fn();
|
|
317
|
+
};
|
|
318
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`Copilot app ${String(frame.type)} timed out`))), options.timeoutMs ?? TIMEOUT_MS);
|
|
319
|
+
stream.send(frame);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function sessionsFrom(frame, focused) {
|
|
323
|
+
const workspaces = Array.isArray(frame.workspaces) ? frame.workspaces.filter(isRecord) : [];
|
|
324
|
+
const sessions = [];
|
|
325
|
+
for (const workspace of workspaces) {
|
|
326
|
+
if (workspace.archivedAt)
|
|
327
|
+
continue;
|
|
328
|
+
const session = isRecord(workspace.session) ? workspace.session : null;
|
|
329
|
+
const id = str(session?.sessionId);
|
|
330
|
+
if (!session || !id)
|
|
331
|
+
continue;
|
|
332
|
+
sessions.push({
|
|
333
|
+
id,
|
|
334
|
+
ref: id.slice(0, 8),
|
|
335
|
+
workspace: str(workspace.id),
|
|
336
|
+
workspaceTitle: str(workspace.projectName) ?? str(workspace.name),
|
|
337
|
+
title: str(session.title) ?? str(workspace.name) ?? "",
|
|
338
|
+
cwd: str(workspace.displayPath),
|
|
339
|
+
// `isRunning` means busy right now, not that an agent exists: every listed session has one.
|
|
340
|
+
agent: "copilot",
|
|
341
|
+
focused: focused(id),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return sessions;
|
|
345
|
+
}
|
|
346
|
+
function createCopilotAppHost(connection, options = {}) {
|
|
347
|
+
const schedule = options.schedule ?? cmux_1.realSchedule;
|
|
348
|
+
const now = options.now ?? Date.now;
|
|
349
|
+
const humanPresent = options.humanPresent ?? copilotAppHumanPresent;
|
|
350
|
+
const openSocket = options.openSocket ?? ((url) => new ws_1.default(url));
|
|
351
|
+
const { stateDir } = connection;
|
|
352
|
+
/** Human messages this host saw live, newer than the log may show yet. */
|
|
353
|
+
const humanAt = new Map();
|
|
354
|
+
const request = (frame, match) => copilotAppRequest(stateDir, frame, match, { openSocket });
|
|
355
|
+
async function workspaceList() {
|
|
356
|
+
const requestId = `ouro-${(0, node_crypto_1.randomUUID)()}`;
|
|
357
|
+
return request({ type: "list_workspaces", request_id: requestId }, (frame) => {
|
|
358
|
+
if (frame.request_id !== requestId)
|
|
359
|
+
return undefined;
|
|
360
|
+
if (frame.type === "error")
|
|
361
|
+
throw new Error(`Copilot app could not list sessions: ${str(frame.message) ?? "error"}`);
|
|
362
|
+
return frame.type === "workspace_list" ? frame : undefined;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function lastHuman(sessionId) {
|
|
366
|
+
let logged = null;
|
|
367
|
+
try {
|
|
368
|
+
logged = readConversation(stateDir, sessionId).humanAt;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// A session with no log yet has no human message in it.
|
|
372
|
+
}
|
|
373
|
+
return Math.max(humanAt.get(sessionId) ?? -Infinity, logged ?? -Infinity);
|
|
374
|
+
}
|
|
375
|
+
async function list() {
|
|
376
|
+
const frame = await workspaceList();
|
|
377
|
+
let present;
|
|
378
|
+
const ages = new Map();
|
|
379
|
+
for (const session of sessionsFrom(frame, () => false))
|
|
380
|
+
ages.set(session.id, now() - lastHuman(session.id));
|
|
381
|
+
if ([...ages.values()].some(age => age >= HUMAN_HOLD_MS && age < PRESENT_HOLD_MS))
|
|
382
|
+
present = await humanPresent();
|
|
383
|
+
return sessionsFrom(frame, (id) => {
|
|
384
|
+
const age = ages.get(id);
|
|
385
|
+
return age < HUMAN_HOLD_MS || (present === true && age < PRESENT_HOLD_MS);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
name: exports.COPILOT_APP_HOST,
|
|
390
|
+
async retirementInventory() {
|
|
391
|
+
const frame = await workspaceList();
|
|
392
|
+
if (!Array.isArray(frame.workspaces) || (Array.isArray(frame.deferred_workspace_ids) && frame.deferred_workspace_ids.length > 0)) {
|
|
393
|
+
throw new Error("invalid Copilot app session inventory");
|
|
394
|
+
}
|
|
395
|
+
const ids = sessionsFrom(frame, () => false).map(session => session.id);
|
|
396
|
+
if (new Set(ids).size !== ids.length)
|
|
397
|
+
throw new Error("invalid Copilot app session inventory identity");
|
|
398
|
+
return ids;
|
|
399
|
+
},
|
|
400
|
+
list,
|
|
401
|
+
async read(sessionId, lines) {
|
|
402
|
+
const conversation = readConversation(stateDir, sessionId);
|
|
403
|
+
// An empty screen is no basis for a judgment; the sense records the error and sends nothing.
|
|
404
|
+
if (conversation.lines.length === 0)
|
|
405
|
+
throw new Error("could not read this Copilot app session's conversation");
|
|
406
|
+
return conversation.lines.slice(-Math.max(1, lines)).join("\n");
|
|
407
|
+
},
|
|
408
|
+
async prompt(sessionId, line) {
|
|
409
|
+
const requestId = `ouro-${(0, node_crypto_1.randomUUID)()}`;
|
|
410
|
+
await request({ type: "send_message", session_id: sessionId, prompt: line, delivery_mode: "enqueue", request_id: requestId }, (frame) => {
|
|
411
|
+
if (frame.request_id !== requestId)
|
|
412
|
+
return undefined;
|
|
413
|
+
if (frame.type === "session_message_sent")
|
|
414
|
+
return true;
|
|
415
|
+
if (frame.type === "session_message_delivery_unknown")
|
|
416
|
+
throw new Error(`Copilot app could not confirm delivery: ${str(frame.detail) ?? "unknown"}`);
|
|
417
|
+
if (frame.type === "error" || frame.type === "session_resume_failed") {
|
|
418
|
+
throw new Error(`Copilot app refused the message: ${str(frame.message) ?? str(frame.reason) ?? String(frame.type)}`);
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
});
|
|
422
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_copilot_app_prompted", message: "sent a message to a Copilot app session", meta: { chars: line.length } });
|
|
423
|
+
},
|
|
424
|
+
async key(sessionId, key) {
|
|
425
|
+
if (key === "ctrl-c" || key === "escape") {
|
|
426
|
+
const requestId = `ouro-${(0, node_crypto_1.randomUUID)()}`;
|
|
427
|
+
await request({ type: "abort_session", session_id: sessionId, request_id: requestId }, (frame) => {
|
|
428
|
+
if (frame.type === "error" && frame.request_id === requestId)
|
|
429
|
+
throw new Error(`Copilot app refused to stop the session: ${str(frame.message) ?? "error"}`);
|
|
430
|
+
const event = isRecord(frame.event) ? frame.event : null;
|
|
431
|
+
return frame.type === "session_event" && frame.session_id === sessionId && event?.type === "session.idle" ? true : undefined;
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const result = PERMISSION_KEYS[key];
|
|
436
|
+
if (!result)
|
|
437
|
+
throw new Error(`the Copilot app has no '${key}' key: send a line, y/1 (approve), 2 (approve for the session), n/3 (reject) or ctrl-c (stop)`);
|
|
438
|
+
const pending = readConversation(stateDir, sessionId).pendingPermission;
|
|
439
|
+
if (!pending)
|
|
440
|
+
throw new Error("this Copilot app session is not waiting for a permission; nothing was sent");
|
|
441
|
+
await request({ type: "respond_to_permission", session_id: sessionId, request_id: pending, result }, (frame) => {
|
|
442
|
+
if (frame.type === "error" && (frame.request_id === pending || frame.session_id === sessionId))
|
|
443
|
+
throw new Error(`Copilot app refused the answer: ${str(frame.message) ?? "error"}`);
|
|
444
|
+
const event = isRecord(frame.event) ? frame.event : null;
|
|
445
|
+
return frame.type === "session_event" && event?.type === "permission.completed" && isRecord(event.data) && event.data.requestId === pending ? true : undefined;
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
async signal(_sessionId, status) {
|
|
449
|
+
// The app has no status line or notification an outside client can set; owner notices carry the news.
|
|
450
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_copilot_app_signal", message: "Copilot app has no status line; Shepherd status kept in its own records", meta: { status: status ?? null } });
|
|
451
|
+
},
|
|
452
|
+
watch(handlers) {
|
|
453
|
+
let closed = false;
|
|
454
|
+
let attempt = 0;
|
|
455
|
+
let stream = null;
|
|
456
|
+
let cancelReconnect = null;
|
|
457
|
+
let cancelInventory = null;
|
|
458
|
+
const pending = new Map();
|
|
459
|
+
/** What each session is waiting on the human for: permission ids and blocking frames. */
|
|
460
|
+
const blocked = new Map();
|
|
461
|
+
const lastBody = new Map();
|
|
462
|
+
const activity = new Map();
|
|
463
|
+
let inventory = [];
|
|
464
|
+
const publish = (error) => {
|
|
465
|
+
handlers.observed?.({
|
|
466
|
+
at: now(),
|
|
467
|
+
sessions: inventory.map((session) => ({ session, activity: activity.get(session.id) ?? "unknown", observedAt: now() })),
|
|
468
|
+
...(error ? { error } : {}),
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
const cancel = (sessionId) => {
|
|
472
|
+
pending.get(sessionId)?.();
|
|
473
|
+
pending.delete(sessionId);
|
|
474
|
+
};
|
|
475
|
+
/** The stop counts only if no new work started during the grace period. */
|
|
476
|
+
const settle = (sessionId, transitionId, backgroundWork) => {
|
|
477
|
+
cancel(sessionId);
|
|
478
|
+
activity.set(sessionId, "idle");
|
|
479
|
+
pending.set(sessionId, schedule(() => {
|
|
480
|
+
pending.delete(sessionId);
|
|
481
|
+
// Only sessions in the workspace list are judged; a side chat's parent is.
|
|
482
|
+
const session = inventory.find(candidate => candidate.id === sessionId);
|
|
483
|
+
if (!session)
|
|
484
|
+
return;
|
|
485
|
+
handlers.returned({
|
|
486
|
+
sessionId, transitionId, agent: session.agent, cwd: session.cwd ?? null,
|
|
487
|
+
workspaceTitle: session.workspaceTitle ?? null, title: session.title,
|
|
488
|
+
lastBody: lastBody.get(sessionId) ?? null, backgroundWork,
|
|
489
|
+
});
|
|
490
|
+
}, GRACE_MS));
|
|
491
|
+
};
|
|
492
|
+
const working = (sessionId) => {
|
|
493
|
+
cancel(sessionId);
|
|
494
|
+
activity.set(sessionId, "working");
|
|
495
|
+
};
|
|
496
|
+
const block = (sessionId, key, transition = key) => {
|
|
497
|
+
const set = blocked.get(sessionId) ?? new Set();
|
|
498
|
+
set.add(key);
|
|
499
|
+
blocked.set(sessionId, set);
|
|
500
|
+
settle(sessionId, `${exports.COPILOT_APP_HOST}:${sessionId}:${transition}`, null);
|
|
501
|
+
};
|
|
502
|
+
/** An answered request; the session is working again unless it still waits on another. */
|
|
503
|
+
const unblock = (sessionId, key) => {
|
|
504
|
+
const set = blocked.get(sessionId);
|
|
505
|
+
set?.delete(key);
|
|
506
|
+
if (!set?.size)
|
|
507
|
+
working(sessionId);
|
|
508
|
+
};
|
|
509
|
+
const onEvent = (sessionId, event) => {
|
|
510
|
+
const type = str(event.type) ?? "";
|
|
511
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
512
|
+
if (QUIET_EVENTS.has(type) || type.endsWith("_delta"))
|
|
513
|
+
return;
|
|
514
|
+
if (humanMessage(event, data)) {
|
|
515
|
+
humanAt.set(sessionId, now());
|
|
516
|
+
handlers.focused(sessionId);
|
|
517
|
+
}
|
|
518
|
+
// A sub-agent's events are its parent's work, never a stop.
|
|
519
|
+
if (event.agentId !== undefined) {
|
|
520
|
+
if (!blocked.get(sessionId)?.size)
|
|
521
|
+
working(sessionId);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const transitionId = `${exports.COPILOT_APP_HOST}:${sessionId}:${str(event.id) ?? `t${now()}`}`;
|
|
525
|
+
switch (type) {
|
|
526
|
+
case "session.idle": {
|
|
527
|
+
blocked.delete(sessionId);
|
|
528
|
+
// A stop the human asked for is theirs to follow up.
|
|
529
|
+
if (data.aborted === true) {
|
|
530
|
+
cancel(sessionId);
|
|
531
|
+
activity.set(sessionId, "idle");
|
|
532
|
+
return handlers.focused(sessionId);
|
|
533
|
+
}
|
|
534
|
+
// The app sends idle only with no background agents or shells in flight; older builds listed them.
|
|
535
|
+
const tasks = isRecord(data.backgroundTasks) ? data.backgroundTasks : null;
|
|
536
|
+
const running = tasks ? [tasks.agents, tasks.shells].some(list => Array.isArray(list) && list.length > 0) : false;
|
|
537
|
+
return settle(sessionId, transitionId, running);
|
|
538
|
+
}
|
|
539
|
+
case "session.shutdown":
|
|
540
|
+
// The runtime ended: nothing is waiting and nothing can be answered.
|
|
541
|
+
blocked.delete(sessionId);
|
|
542
|
+
cancel(sessionId);
|
|
543
|
+
activity.set(sessionId, "idle");
|
|
544
|
+
return;
|
|
545
|
+
case "session.error":
|
|
546
|
+
if (data.eligibleForAutoSwitch === true)
|
|
547
|
+
break;
|
|
548
|
+
lastBody.set(sessionId, `Error: ${str(data.message) ?? str(data.errorType) ?? "unknown"}`);
|
|
549
|
+
return settle(sessionId, transitionId, null);
|
|
550
|
+
case "permission.requested":
|
|
551
|
+
if (data.resolvedByHook === true || !str(data.requestId))
|
|
552
|
+
break;
|
|
553
|
+
return block(sessionId, data.requestId);
|
|
554
|
+
case "permission.completed":
|
|
555
|
+
return unblock(sessionId, str(data.requestId) ?? "");
|
|
556
|
+
case "assistant.message":
|
|
557
|
+
if (str(data.content))
|
|
558
|
+
lastBody.set(sessionId, data.content);
|
|
559
|
+
break;
|
|
560
|
+
case "session.task_complete":
|
|
561
|
+
if (str(data.summary))
|
|
562
|
+
lastBody.set(sessionId, data.summary);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
// A turn end is one model step, not a stop, and a waiting request keeps the session waiting.
|
|
566
|
+
if (type === "assistant.turn_end" || blocked.get(sessionId)?.size)
|
|
567
|
+
return;
|
|
568
|
+
working(sessionId);
|
|
569
|
+
};
|
|
570
|
+
const onFrame = (frame) => {
|
|
571
|
+
if (closed)
|
|
572
|
+
return;
|
|
573
|
+
const type = str(frame.type) ?? "";
|
|
574
|
+
if (type === "server_hello") {
|
|
575
|
+
attempt = 0;
|
|
576
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_copilot_app_connected", message: "Copilot app event stream connected", meta: {} });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (type === "workspace_list") {
|
|
580
|
+
// Advisory only: the send path re-checks focus through list().
|
|
581
|
+
inventory = sessionsFrom(frame, (id) => now() - (humanAt.get(id) ?? -Infinity) < HUMAN_HOLD_MS);
|
|
582
|
+
for (const id of [...activity.keys()])
|
|
583
|
+
if (!inventory.some(session => session.id === id))
|
|
584
|
+
activity.delete(id);
|
|
585
|
+
return publish();
|
|
586
|
+
}
|
|
587
|
+
const sessionId = str(frame.session_id);
|
|
588
|
+
if (!sessionId)
|
|
589
|
+
return;
|
|
590
|
+
if (type === "session_event" && isRecord(frame.event)) {
|
|
591
|
+
// History replays describe the past, never a new stop.
|
|
592
|
+
if (!frame.history_request_id)
|
|
593
|
+
onEvent(sessionId, frame.event);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// A request without an id is keyed by its kind, so its resolution can still clear it.
|
|
597
|
+
if (BLOCKED_FRAMES.has(type)) {
|
|
598
|
+
const id = str(frame.request_id);
|
|
599
|
+
return block(sessionId, id ?? type, id ?? `${type}@${now()}`);
|
|
600
|
+
}
|
|
601
|
+
if (RESOLVED_FRAMES.has(type))
|
|
602
|
+
return unblock(sessionId, str(frame.request_id) ?? type.replace(/_resolved$/, "_request"));
|
|
603
|
+
};
|
|
604
|
+
// Reconnect and close cancel the next refresh, so it only ever runs on a live stream.
|
|
605
|
+
const refreshInventory = (current) => {
|
|
606
|
+
current.send({ type: "list_workspaces", request_id: `ouro-${(0, node_crypto_1.randomUUID)()}` });
|
|
607
|
+
cancelInventory = schedule(() => refreshInventory(current), INVENTORY_MS);
|
|
608
|
+
};
|
|
609
|
+
const reconnect = (error) => {
|
|
610
|
+
stream = null;
|
|
611
|
+
cancelInventory?.();
|
|
612
|
+
cancelInventory = null;
|
|
613
|
+
// Answers given while disconnected are not replayed, so nothing stays waiting across a gap.
|
|
614
|
+
blocked.clear();
|
|
615
|
+
if (closed)
|
|
616
|
+
return;
|
|
617
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
618
|
+
attempt += 1;
|
|
619
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_copilot_app_closed", message: "Copilot app event stream closed; reconnecting", meta: { delay, error: error?.message ?? null } });
|
|
620
|
+
publish("host_unavailable");
|
|
621
|
+
cancelReconnect = schedule(connect, delay);
|
|
622
|
+
};
|
|
623
|
+
const connect = () => {
|
|
624
|
+
cancelReconnect = null;
|
|
625
|
+
try {
|
|
626
|
+
// Stops while disconnected are not replayed; the next idle or permission event is.
|
|
627
|
+
stream = openStream(copilotAppUrl(stateDir), openSocket, onFrame, reconnect);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
return reconnect(error);
|
|
631
|
+
}
|
|
632
|
+
refreshInventory(stream);
|
|
633
|
+
};
|
|
634
|
+
connect();
|
|
635
|
+
return {
|
|
636
|
+
close() {
|
|
637
|
+
closed = true;
|
|
638
|
+
cancelReconnect?.();
|
|
639
|
+
cancelInventory?.();
|
|
640
|
+
for (const stop of pending.values())
|
|
641
|
+
stop();
|
|
642
|
+
pending.clear();
|
|
643
|
+
stream?.close();
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
@@ -4,10 +4,13 @@ exports.createShepherdHost = createShepherdHost;
|
|
|
4
4
|
const runtime_1 = require("../../nerves/runtime");
|
|
5
5
|
const client_1 = require("./client");
|
|
6
6
|
const cmux_1 = require("./cmux");
|
|
7
|
+
const copilot_app_1 = require("./copilot-app");
|
|
7
8
|
const herdr_1 = require("./herdr");
|
|
8
9
|
/** The host for this machine's connection. */
|
|
9
10
|
function createShepherdHost(connection, options) {
|
|
10
11
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_host_created", message: "opened the Shepherd terminal host", meta: { host: connection.host } });
|
|
12
|
+
if (connection.host === "copilot-app")
|
|
13
|
+
return (0, copilot_app_1.createCopilotAppHost)(connection, { schedule: options.schedule, now: options.now });
|
|
11
14
|
return connection.host === "herdr"
|
|
12
15
|
? (0, herdr_1.createHerdrHost)(connection, { schedule: options.schedule })
|
|
13
16
|
: (0, cmux_1.createCmuxHost)((0, client_1.createCmuxClient)(connection, { agentName: options.agentName }), { now: options.now, schedule: options.schedule });
|
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.960",
|
|
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.960",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|