baychat 0.34.1 → 0.34.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -2
- package/dist/bays-command.js +6 -0
- package/dist/channel/feed.js +48 -4
- package/dist/channel/log-file.js +37 -0
- package/dist/channel/server.js +34 -4
- package/dist/claude-context-flush.js +31 -8
- package/dist/claude-context.js +19 -5
- package/dist/claude-onboarding.js +21 -8
- package/dist/cli-help.js +3 -0
- package/dist/client-paths.js +6 -1
- package/dist/client-scope.js +34 -0
- package/dist/commands.js +16 -8
- package/dist/launch/command.js +6 -3
- package/dist/main.js +14 -1
- package/dist/mcp-files.js +2 -0
- package/dist/mcp-register.js +2 -0
- package/dist/protocol-content.js +1 -1
- package/dist/refresh-clients.js +7 -0
- package/dist/relay/acp/approval.js +99 -30
- package/dist/relay/acp/daemon-glue.js +2 -1
- package/dist/relay/adapters.js +5 -1
- package/dist/relay/bay-relays.js +18 -5
- package/dist/relay/codex-allowances.js +113 -0
- package/dist/relay/codex-app-server.js +637 -157
- package/dist/relay/codex-approvals.js +390 -0
- package/dist/relay/codex-commands.js +61 -0
- package/dist/relay/codex-log.js +260 -0
- package/dist/relay/codex-runner.js +505 -0
- package/dist/relay/codex-version.js +50 -0
- package/dist/relay/daemon.js +190 -6
- package/dist/relay/fresh-start.js +2 -1
- package/dist/relay/launcher/capabilities.js +24 -9
- package/dist/relay/launcher/codex-launch.js +9 -2
- package/dist/relay/launcher/codex-relay-launch.js +80 -0
- package/dist/relay/launcher/handle.js +101 -30
- package/dist/relay/launcher/loop.js +5 -1
- package/dist/relay/launcher/stop-registered.js +124 -9
- package/dist/relay/registry.js +30 -0
- package/dist/relay/runtime-limits.js +167 -0
- package/dist/relay/spawn-env.js +40 -3
- package/dist/relay/terminal-pane.js +1 -0
- package/dist/skill-bootstrap.js +2 -0
- package/dist/tool-defs.js +30 -0
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askPhone = askPhone;
|
|
4
|
+
exports.askPhoneChoice = askPhoneChoice;
|
|
4
5
|
const api_1 = require("../../api");
|
|
5
6
|
const approve_hook_1 = require("../../approve-hook");
|
|
7
|
+
const waiting_cards_1 = require("../../waiting-cards");
|
|
6
8
|
const HOLD_SECONDS = 25; // under the usual 60s proxy idle timeout — same as approve-hook
|
|
7
9
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
8
10
|
const RETRY_DELAY_MS = 2_000;
|
|
@@ -13,53 +15,120 @@ const ALLOW_INDEX = 0; // the ONLY index that means yes
|
|
|
13
15
|
* NEVER THROWS, and every path that is not an explicit "Allow this once" is a refusal: an
|
|
14
16
|
* unreachable server, a status we do not know, a withdrawn card, the deadline. The card itself
|
|
15
17
|
* never expires server-side; our deadline exists so a turn cannot hold a process forever, and
|
|
16
|
-
* reaching it REFUSES
|
|
18
|
+
* reaching it REFUSES — and now also takes the card back, so a late tap on it answers nothing.
|
|
19
|
+
* Silence is never a yes.
|
|
17
20
|
*/
|
|
18
21
|
async function askPhone(params, deps = {}) {
|
|
22
|
+
const answer = await askPhoneChoice({ ...params, options: approve_hook_1.APPROVE_OPTIONS }, deps);
|
|
23
|
+
return { allowed: answer.index === ALLOW_INDEX, reason: answer.reason };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A decision card with the caller's own buttons. Resolves the tapped index, or `index: null` with
|
|
27
|
+
* the reason nobody's answer counts — the deadline, an abort, a withdrawn card, an unreachable or
|
|
28
|
+
* confused server. What an index MEANS is the caller's; `null` must always be read as a refusal.
|
|
29
|
+
*
|
|
30
|
+
* NEVER THROWS. When it gives up on a card it created (the deadline, or `signal`), it withdraws
|
|
31
|
+
* it (`POST /approvals/:id/withdraw`, best effort), so the phone stops offering a question whose
|
|
32
|
+
* answer would reach nobody.
|
|
33
|
+
*/
|
|
34
|
+
async function askPhoneChoice(params, deps = {}) {
|
|
19
35
|
const request = deps.request ?? api_1.apiRequest;
|
|
20
36
|
const now = deps.now ?? Date.now;
|
|
21
37
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
38
|
+
const markWaiting = deps.markWaiting ?? waiting_cards_1.markWaitingCard;
|
|
22
39
|
const waitSeconds = params.waitSeconds ?? approve_hook_1.DEFAULT_WAIT_SECONDS;
|
|
23
40
|
const deadline = now() + waitSeconds * 1000;
|
|
24
41
|
const hold = () => Math.max(1, Math.min(HOLD_SECONDS, Math.ceil((deadline - now()) / 1000)));
|
|
25
|
-
const no = (reason) => ({
|
|
42
|
+
const no = (reason) => ({ index: null, reason });
|
|
43
|
+
const label = (index, answer) => answer ?? (typeof index === "number" ? params.options[index] : undefined) ?? "no answer";
|
|
44
|
+
const withdraw = async (requestId) => {
|
|
45
|
+
try {
|
|
46
|
+
await request(params.auth, "POST", `/api/device-api/approvals/${encodeURIComponent(requestId)}/withdraw`, {});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A card left pending is harmless — its answer reaches nobody. Nothing to do but move on.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** The next reply, or "aborted" the moment `signal` fires — a long-poll is not waited out. */
|
|
53
|
+
const orAbort = (pending) => {
|
|
54
|
+
const signal = params.signal;
|
|
55
|
+
if (!signal)
|
|
56
|
+
return pending;
|
|
57
|
+
if (signal.aborted)
|
|
58
|
+
return Promise.resolve("aborted");
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const onAbort = () => resolve("aborted");
|
|
61
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
pending.then((value) => {
|
|
63
|
+
signal.removeEventListener("abort", onAbort);
|
|
64
|
+
resolve(value);
|
|
65
|
+
}, (err) => {
|
|
66
|
+
signal.removeEventListener("abort", onAbort);
|
|
67
|
+
reject(err);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
if (params.signal?.aborted)
|
|
72
|
+
return no("nobody needed the answer any more");
|
|
26
73
|
try {
|
|
27
|
-
|
|
74
|
+
// With a signal the first hold is ONE second, so a stop that lands while the card is being
|
|
75
|
+
// created is seen at once — never after a 25-second hold.
|
|
76
|
+
const creating = request(params.auth, "POST", "/api/device-api/approvals", {
|
|
28
77
|
session: params.session,
|
|
29
|
-
conversationId: params.conversationId,
|
|
78
|
+
...(params.direct ? { direct: true } : { conversationId: params.conversationId }),
|
|
30
79
|
question: params.question,
|
|
31
|
-
options: [...
|
|
32
|
-
|
|
80
|
+
options: [...params.options],
|
|
81
|
+
...(params.guardedOptions?.length ? { guardedOptions: params.guardedOptions } : {}),
|
|
82
|
+
wait: params.signal ? 1 : hold(),
|
|
33
83
|
});
|
|
84
|
+
let reply = await orAbort(creating);
|
|
85
|
+
if (reply === "aborted") {
|
|
86
|
+
// The card is (being) created and nobody wants its answer: take it back the moment its id is known.
|
|
87
|
+
void creating.then((created) => (typeof created.requestId === "string" && created.requestId ? withdraw(created.requestId) : undefined)).catch(() => undefined);
|
|
88
|
+
return no("nobody needed the answer any more — the card was withdrawn");
|
|
89
|
+
}
|
|
34
90
|
const requestId = typeof reply.requestId === "string" ? reply.requestId : "";
|
|
35
91
|
if (!requestId)
|
|
36
92
|
return no("BayChat did not return an approval id");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
const unmark = markWaiting(requestId);
|
|
94
|
+
try {
|
|
95
|
+
let failures = 0;
|
|
96
|
+
for (;;) {
|
|
97
|
+
if (params.signal?.aborted)
|
|
98
|
+
reply = "aborted";
|
|
99
|
+
if (reply === "aborted") {
|
|
100
|
+
await withdraw(requestId);
|
|
101
|
+
return no("nobody needed the answer any more — the card was withdrawn");
|
|
102
|
+
}
|
|
103
|
+
if (reply.status === "answered") {
|
|
104
|
+
const index = typeof reply.answerIndex === "number" ? reply.answerIndex : null;
|
|
105
|
+
return { index, reason: `answered "${label(index, reply.answer)}" in BayChat` };
|
|
106
|
+
}
|
|
107
|
+
if (reply.status === "cancelled")
|
|
108
|
+
return no("the decision was withdrawn in BayChat");
|
|
109
|
+
if (reply.status !== "waiting")
|
|
110
|
+
return no(`BayChat answered with a status this relay does not understand ("${String(reply.status)}")`);
|
|
111
|
+
if (deadline - now() <= 0) {
|
|
112
|
+
await withdraw(requestId);
|
|
113
|
+
return no(`nobody answered within ${waitSeconds}s — the card was withdrawn, and this request was refused`);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
reply = await orAbort(request(params.auth, "GET", `/api/device-api/approvals/${encodeURIComponent(requestId)}?wait=${hold()}`));
|
|
117
|
+
failures = 0;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err instanceof api_1.ApiError && err.status < 500 && err.status !== 429)
|
|
121
|
+
return no(`BayChat refused the wait — ${err.message}`);
|
|
122
|
+
failures += 1;
|
|
123
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES)
|
|
124
|
+
return no(`BayChat became unreachable while waiting — ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
await sleep(RETRY_DELAY_MS);
|
|
126
|
+
}
|
|
61
127
|
}
|
|
62
128
|
}
|
|
129
|
+
finally {
|
|
130
|
+
unmark();
|
|
131
|
+
}
|
|
63
132
|
}
|
|
64
133
|
catch (err) {
|
|
65
134
|
return no(`BayChat could not be asked — ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -193,7 +193,8 @@ function registerAcpFrame(frame, deps) {
|
|
|
193
193
|
* ever. `upsert` merges, so an attach over it would keep `acp` and give the name two drivers.
|
|
194
194
|
*/
|
|
195
195
|
function acpAttachRefusal(registry, session) {
|
|
196
|
-
|
|
196
|
+
const target = registry.get(session);
|
|
197
|
+
return target?.acp || target?.codex
|
|
197
198
|
? `"${session}" is run by the relay on this computer; attach a terminal under a different session name.`
|
|
198
199
|
: undefined;
|
|
199
200
|
}
|
package/dist/relay/adapters.js
CHANGED
|
@@ -196,7 +196,7 @@ const codexAdapter = {
|
|
|
196
196
|
* interface is marked `[experimental]` by OpenAI and its shape is not frozen,
|
|
197
197
|
* so there has to be a way back that does not require a new release.
|
|
198
198
|
*/
|
|
199
|
-
async runTurn({ binaryPath, target, prompt }) {
|
|
199
|
+
async runTurn({ binaryPath, target, prompt, policy, onApproval, onNotification, log }) {
|
|
200
200
|
if (process.env.BAYCHAT_CODEX_TRANSPORT === "exec") {
|
|
201
201
|
return {
|
|
202
202
|
kind: "failed",
|
|
@@ -209,6 +209,10 @@ const codexAdapter = {
|
|
|
209
209
|
threadId: target.resumeId,
|
|
210
210
|
prompt,
|
|
211
211
|
cwd: target.resumeCwd ?? target.cwd,
|
|
212
|
+
...(policy ? { policy } : {}),
|
|
213
|
+
...(onApproval ? { onApproval } : {}),
|
|
214
|
+
...(onNotification ? { onNotification } : {}),
|
|
215
|
+
...(log ? { log } : {}),
|
|
212
216
|
}, { spawn: child_process_1.spawn });
|
|
213
217
|
},
|
|
214
218
|
};
|
package/dist/relay/bay-relays.js
CHANGED
|
@@ -55,17 +55,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
55
55
|
};
|
|
56
56
|
})();
|
|
57
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
-
exports.BayRelaySupervisor = exports.BAY_RELAY_PARENT_ENV = exports.BAY_RELAY_CHILD_ENV = void 0;
|
|
58
|
+
exports.BayRelaySupervisor = exports.BAY_RELAY_PARENT_ENV = exports.BAY_RELAY_MCP_ENTRY_ENV = exports.BAY_RELAY_CHILD_ENV = void 0;
|
|
59
59
|
exports.bayRelayScope = bayRelayScope;
|
|
60
60
|
exports.bayClientEnv = bayClientEnv;
|
|
61
61
|
exports.bayRelayEnv = bayRelayEnv;
|
|
62
|
+
exports.relayMcpEntry = relayMcpEntry;
|
|
62
63
|
exports.isBayRelayChild = isBayRelayChild;
|
|
63
64
|
const node_child_process_1 = require("node:child_process");
|
|
64
65
|
const path = __importStar(require("node:path"));
|
|
66
|
+
const bays_1 = require("../bays");
|
|
65
67
|
const socket_1 = require("./socket");
|
|
66
68
|
const log_line_1 = require("./log-line");
|
|
67
69
|
/** Set in a per-Bay child relay: it must never supervise children of its own. */
|
|
68
70
|
exports.BAY_RELAY_CHILD_ENV = "BAYCHAT_RELAY_BAY";
|
|
71
|
+
/** The MCP entry name this child's Bay has in the computer's client configs (`baychat-<bay>`):
|
|
72
|
+
* a child cannot work it out itself — its credentials hold only its own Bay's token. */
|
|
73
|
+
exports.BAY_RELAY_MCP_ENTRY_ENV = "BAYCHAT_RELAY_MCP_ENTRY";
|
|
69
74
|
/** The supervising relay's pid, so an orphaned child can stop itself. */
|
|
70
75
|
exports.BAY_RELAY_PARENT_ENV = "BAYCHAT_RELAY_PARENT_PID";
|
|
71
76
|
/** What a Bay id may contribute to a path or pipe name. */
|
|
@@ -97,8 +102,11 @@ function bayClientEnv(scope) {
|
|
|
97
102
|
};
|
|
98
103
|
}
|
|
99
104
|
/** The environment a per-Bay CHILD relay runs with. Holds the Bay's token: never logged. */
|
|
100
|
-
function bayRelayEnv(bay, baseUrl, scope, parentPid
|
|
105
|
+
function bayRelayEnv(bay, baseUrl, scope, parentPid,
|
|
106
|
+
/** The Bay's MCP entry name among the computer's Bays (`bayEntryName`). */
|
|
107
|
+
mcpEntry) {
|
|
101
108
|
return {
|
|
109
|
+
...(mcpEntry ? { [exports.BAY_RELAY_MCP_ENTRY_ENV]: mcpEntry } : {}),
|
|
102
110
|
...bayClientEnv(scope),
|
|
103
111
|
BAYCHAT_DEVICE_TOKEN: bay.token,
|
|
104
112
|
BAYCHAT_API_URL: baseUrl,
|
|
@@ -107,6 +115,11 @@ function bayRelayEnv(bay, baseUrl, scope, parentPid) {
|
|
|
107
115
|
[exports.BAY_RELAY_PARENT_ENV]: String(parentPid),
|
|
108
116
|
};
|
|
109
117
|
}
|
|
118
|
+
/** The MCP entry this relay's Bay has in Codex's and the other clients' configs: the default
|
|
119
|
+
* Bay's is `baychat`, a per-Bay child's the name its parent gave it. */
|
|
120
|
+
function relayMcpEntry(env = process.env) {
|
|
121
|
+
return env[exports.BAY_RELAY_MCP_ENTRY_ENV] || bays_1.DEFAULT_MCP_ENTRY;
|
|
122
|
+
}
|
|
110
123
|
/** Is THIS process a per-Bay child relay? */
|
|
111
124
|
function isBayRelayChild(env = process.env) {
|
|
112
125
|
return Boolean(env[exports.BAY_RELAY_CHILD_ENV]);
|
|
@@ -164,14 +177,14 @@ class BayRelaySupervisor {
|
|
|
164
177
|
const exited = this.lastExit.get(bayId);
|
|
165
178
|
if (exited !== undefined && this.now() - exited < (this.deps.restartDelayMs ?? 10_000))
|
|
166
179
|
continue;
|
|
167
|
-
this.startChild(bay, baseUrl);
|
|
180
|
+
this.startChild(bay, baseUrl, (0, bays_1.bayEntryName)(bay, bays));
|
|
168
181
|
}
|
|
169
182
|
}
|
|
170
|
-
startChild(bay, baseUrl) {
|
|
183
|
+
startChild(bay, baseUrl, mcpEntry) {
|
|
171
184
|
const scope = bayRelayScope(bay.bayId, this.deps.baseConfigDir, this.deps.baseSocket);
|
|
172
185
|
let child;
|
|
173
186
|
try {
|
|
174
|
-
child = this.spawnChild(bayRelayEnv(bay, baseUrl, scope, process.pid));
|
|
187
|
+
child = this.spawnChild(bayRelayEnv(bay, baseUrl, scope, process.pid, mcpEntry));
|
|
175
188
|
}
|
|
176
189
|
catch (err) {
|
|
177
190
|
this.lastExit.set(bay.bayId, this.now());
|
|
@@ -0,0 +1,113 @@
|
|
|
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.CodexAllowances = void 0;
|
|
37
|
+
exports.completedFileChangePaths = completedFileChangePaths;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const codex_approvals_1 = require("./codex-approvals");
|
|
40
|
+
/**
|
|
41
|
+
* "Allow for this session" answers, per BayChat session AND per Codex thread (review 2026-10-23).
|
|
42
|
+
*
|
|
43
|
+
* Codex's own `acceptForSession` dies with the per-turn `codex app-server` process, so the relay
|
|
44
|
+
* remembers the answer for later turns — in memory only, never on disk. A remembered answer
|
|
45
|
+
* belongs to ONE conversation: when the session's thread changes (`/new`, a fresh start, a
|
|
46
|
+
* terminal Codex that registered a new thread) the answers are gone, and they are forgotten
|
|
47
|
+
* outright whenever the relay forgets or re-registers the session (End, a dead-session sweep, a
|
|
48
|
+
* new attach or registration, `/clear`).
|
|
49
|
+
*
|
|
50
|
+
* "This exact command" is not "this exact effect": a remembered command runs whatever its scripts
|
|
51
|
+
* contain when it runs. So a file change Codex completes under a remembered command's folder drops
|
|
52
|
+
* every command remembered there, and the next run is asked again.
|
|
53
|
+
*/
|
|
54
|
+
class CodexAllowances {
|
|
55
|
+
bySession = new Map();
|
|
56
|
+
/** The answers for this session's current thread (a new, empty set when the thread changed). */
|
|
57
|
+
for(session, thread) {
|
|
58
|
+
const held = this.bySession.get(session);
|
|
59
|
+
if (held && held.thread === thread)
|
|
60
|
+
return held.keys;
|
|
61
|
+
const fresh = { thread, keys: new Set() };
|
|
62
|
+
this.bySession.set(session, fresh);
|
|
63
|
+
return fresh.keys;
|
|
64
|
+
}
|
|
65
|
+
/** A thread that had no id yet (a new one) now has one: its answers move with it. */
|
|
66
|
+
rebind(session, from, to) {
|
|
67
|
+
const held = this.bySession.get(session);
|
|
68
|
+
if (held && held.thread === from)
|
|
69
|
+
held.thread = to;
|
|
70
|
+
}
|
|
71
|
+
forget(session) {
|
|
72
|
+
this.bySession.delete(session);
|
|
73
|
+
}
|
|
74
|
+
count(session) {
|
|
75
|
+
return this.bySession.get(session)?.keys.size ?? 0;
|
|
76
|
+
}
|
|
77
|
+
/** Files changed: drop the commands remembered in any folder they are under. Returns how many. */
|
|
78
|
+
noteFileChanges(session, paths) {
|
|
79
|
+
const held = this.bySession.get(session);
|
|
80
|
+
if (!held || paths.length === 0)
|
|
81
|
+
return 0;
|
|
82
|
+
let dropped = 0;
|
|
83
|
+
for (const key of [...held.keys]) {
|
|
84
|
+
const cwd = (0, codex_approvals_1.commandKeyCwd)(key);
|
|
85
|
+
if (cwd === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
const under = paths.some((p) => {
|
|
88
|
+
const abs = path.resolve(cwd || "/", p);
|
|
89
|
+
const rel = path.relative(cwd || "/", abs);
|
|
90
|
+
return !cwd || rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
91
|
+
});
|
|
92
|
+
if (under) {
|
|
93
|
+
held.keys.delete(key);
|
|
94
|
+
dropped += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return dropped;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.CodexAllowances = CodexAllowances;
|
|
101
|
+
/** The paths of a completed file change, from an `item/completed` notification; [] for anything else. */
|
|
102
|
+
function completedFileChangePaths(method, params) {
|
|
103
|
+
if (method !== "item/completed")
|
|
104
|
+
return [];
|
|
105
|
+
const item = (params.item ?? {});
|
|
106
|
+
if (item.type !== "fileChange" || item.status !== "completed" || !Array.isArray(item.changes))
|
|
107
|
+
return [];
|
|
108
|
+
return item.changes.flatMap((c) => {
|
|
109
|
+
const change = (c ?? {});
|
|
110
|
+
const kind = (change.kind ?? {});
|
|
111
|
+
return [change.path, kind.move_path].filter((p) => typeof p === "string" && p !== "");
|
|
112
|
+
});
|
|
113
|
+
}
|