antechamber 0.1.1 → 0.2.1
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.md +45 -0
- package/README.md +24 -14
- package/bin/opencode-antechamber +14 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +10 -6
- package/dist/pi-event-translation.js +237 -80
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +113 -65
- package/dist/routes-v2.js +407 -95
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +54 -13
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.d.ts +1 -1
- package/dist/session-history.js +110 -44
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +191 -38
- package/dist/session-tree.d.ts +10 -2
- package/dist/session-tree.js +55 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +331 -120
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/vcs.d.ts +5 -0
- package/dist/vcs.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
2
|
export { isLoopbackHost, serverPassword } from "./http.js";
|
|
3
|
-
|
|
3
|
+
interface AdapterServerOptions {
|
|
4
4
|
port: number;
|
|
5
5
|
host: string;
|
|
6
6
|
/** Fallback directory when requests carry no ?directory= param. */
|
|
@@ -12,6 +12,9 @@ export declare class AdapterServer {
|
|
|
12
12
|
private sessions;
|
|
13
13
|
private sse;
|
|
14
14
|
private sseClients;
|
|
15
|
+
private webSocketClients;
|
|
16
|
+
private webSocketEvents;
|
|
17
|
+
private nextWebSocketEventID;
|
|
15
18
|
private catalogPromise;
|
|
16
19
|
private actualPort;
|
|
17
20
|
private readonly http;
|
|
@@ -23,6 +26,11 @@ export declare class AdapterServer {
|
|
|
23
26
|
private broadcast;
|
|
24
27
|
private writeSSE;
|
|
25
28
|
private handleSSE;
|
|
29
|
+
private handleUpgrade;
|
|
30
|
+
private rejectUpgrade;
|
|
31
|
+
private broadcastWebSocket;
|
|
32
|
+
private writeWebSocketEvent;
|
|
33
|
+
private writeWebSocket;
|
|
26
34
|
private route;
|
|
27
35
|
private handle;
|
|
28
36
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import { debugLog } from "./misc-routes.js";
|
|
4
5
|
import { SessionManager } from "./sessions.js";
|
|
@@ -14,17 +15,22 @@ export class AdapterServer {
|
|
|
14
15
|
sessions;
|
|
15
16
|
sse;
|
|
16
17
|
sseClients = new Map();
|
|
18
|
+
webSocketClients = new Set();
|
|
19
|
+
webSocketEvents = [];
|
|
20
|
+
nextWebSocketEventID = 0;
|
|
17
21
|
catalogPromise;
|
|
18
22
|
actualPort;
|
|
19
|
-
http = new Http();
|
|
23
|
+
http = new Http(() => this.url);
|
|
20
24
|
constructor(options) {
|
|
21
25
|
this.options = options;
|
|
22
26
|
this.sse = new SSE(options, this.sseClients);
|
|
23
27
|
this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
|
|
24
28
|
this.server = createServer((req, res) => void this.route(req, res));
|
|
29
|
+
this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
25
30
|
}
|
|
26
31
|
catalog() {
|
|
27
32
|
this.catalogPromise ??= loadCatalog(this.options.defaultDirectory).catch((err) => {
|
|
33
|
+
this.catalogPromise = undefined;
|
|
28
34
|
console.error("[catalog] failed to load models from pi:", err);
|
|
29
35
|
return { providers: [], defaults: {} };
|
|
30
36
|
});
|
|
@@ -50,6 +56,9 @@ export class AdapterServer {
|
|
|
50
56
|
// session RPC groups and private task hosts are still being disposed.
|
|
51
57
|
const closed = new Promise((resolve) => this.server.close(() => resolve()));
|
|
52
58
|
this.sse.close();
|
|
59
|
+
for (const socket of this.webSocketClients)
|
|
60
|
+
socket.destroy();
|
|
61
|
+
this.webSocketClients.clear();
|
|
53
62
|
try {
|
|
54
63
|
await this.sessions.disposeAll();
|
|
55
64
|
}
|
|
@@ -64,12 +73,91 @@ export class AdapterServer {
|
|
|
64
73
|
// ---------------------------------------------------------------
|
|
65
74
|
broadcast(directory, event) {
|
|
66
75
|
this.sse.broadcast(directory, event);
|
|
76
|
+
this.broadcastWebSocket(directory, event);
|
|
67
77
|
}
|
|
68
78
|
writeSSE(res, frame) {
|
|
69
79
|
return this.sse.writeSSE(res, frame);
|
|
70
80
|
}
|
|
71
|
-
handleSSE(res, flavor) {
|
|
72
|
-
|
|
81
|
+
handleSSE(req, res, flavor, directory, global) {
|
|
82
|
+
const lastEventID = req.headers["last-event-id"];
|
|
83
|
+
this.sse.handleSSE(res, { flavor, global, ...(global ? {} : { directory }) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
|
|
84
|
+
}
|
|
85
|
+
handleUpgrade(req, socket, _head) {
|
|
86
|
+
const url = new URL(req.url ?? "/", this.url);
|
|
87
|
+
if (url.pathname !== "/api/global/event/ws") {
|
|
88
|
+
this.rejectUpgrade(socket, 404, "Not Found");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!this.http.allowsUpgrade(req)) {
|
|
92
|
+
this.rejectUpgrade(socket, 403, "Forbidden");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const keyHeader = req.headers["sec-websocket-key"];
|
|
96
|
+
const key = Array.isArray(keyHeader) ? keyHeader[0] : keyHeader;
|
|
97
|
+
if (req.headers["sec-websocket-version"] !== "13" || typeof key !== "string") {
|
|
98
|
+
this.rejectUpgrade(socket, 400, "Bad Request");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
102
|
+
socket.write([
|
|
103
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
104
|
+
"Upgrade: websocket",
|
|
105
|
+
"Connection: Upgrade",
|
|
106
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
107
|
+
"",
|
|
108
|
+
"",
|
|
109
|
+
].join("\r\n"));
|
|
110
|
+
this.webSocketClients.add(socket);
|
|
111
|
+
socket.once("close", () => this.webSocketClients.delete(socket));
|
|
112
|
+
socket.once("error", () => this.webSocketClients.delete(socket));
|
|
113
|
+
const cursor = Number(url.searchParams.get("lastEventId"));
|
|
114
|
+
const replayReset = Number.isSafeInteger(cursor) && cursor >= 0 && cursor < this.nextWebSocketEventID - this.webSocketEvents.length;
|
|
115
|
+
this.writeWebSocket(socket, { type: "ready", ...(replayReset ? { replayReset: true } : {}) });
|
|
116
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
117
|
+
return;
|
|
118
|
+
for (const event of this.webSocketEvents) {
|
|
119
|
+
if (event.id > cursor)
|
|
120
|
+
this.writeWebSocketEvent(socket, event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rejectUpgrade(socket, status, message) {
|
|
124
|
+
socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\n\r\n`);
|
|
125
|
+
}
|
|
126
|
+
broadcastWebSocket(directory, payload) {
|
|
127
|
+
const event = { id: ++this.nextWebSocketEventID, directory, payload };
|
|
128
|
+
this.webSocketEvents.push(event);
|
|
129
|
+
if (this.webSocketEvents.length > 1024)
|
|
130
|
+
this.webSocketEvents.shift();
|
|
131
|
+
for (const socket of this.webSocketClients)
|
|
132
|
+
this.writeWebSocketEvent(socket, event);
|
|
133
|
+
}
|
|
134
|
+
writeWebSocketEvent(socket, event) {
|
|
135
|
+
this.writeWebSocket(socket, {
|
|
136
|
+
type: "event",
|
|
137
|
+
eventId: String(event.id),
|
|
138
|
+
directory: event.directory,
|
|
139
|
+
scope: "global",
|
|
140
|
+
payload: event.payload,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
writeWebSocket(socket, frame) {
|
|
144
|
+
if (socket.destroyed || !socket.writable) {
|
|
145
|
+
this.webSocketClients.delete(socket);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const payload = Buffer.from(JSON.stringify(frame));
|
|
149
|
+
const header = payload.length < 126
|
|
150
|
+
? Buffer.from([0x81, payload.length])
|
|
151
|
+
: payload.length <= 0xffff
|
|
152
|
+
? Buffer.from([0x81, 126, payload.length >> 8, payload.length & 0xff])
|
|
153
|
+
: (() => {
|
|
154
|
+
const value = Buffer.alloc(10);
|
|
155
|
+
value[0] = 0x81;
|
|
156
|
+
value[1] = 127;
|
|
157
|
+
value.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
158
|
+
return value;
|
|
159
|
+
})();
|
|
160
|
+
socket.write(Buffer.concat([header, payload]));
|
|
73
161
|
}
|
|
74
162
|
// ---------------------------------------------------------------
|
|
75
163
|
// Router
|
|
@@ -82,18 +170,30 @@ export class AdapterServer {
|
|
|
82
170
|
const url = new URL(req.url ?? "/", this.url);
|
|
83
171
|
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
84
172
|
const method = req.method ?? "GET";
|
|
85
|
-
const
|
|
173
|
+
const directoryQuery = url.searchParams.get("directory") ??
|
|
86
174
|
url.searchParams.get("location[directory]") ??
|
|
87
|
-
url.searchParams.get("location")
|
|
88
|
-
|
|
89
|
-
|
|
175
|
+
url.searchParams.get("location");
|
|
176
|
+
const directoryHeader = req.headers["x-opencode-directory"];
|
|
177
|
+
const encodedDirectory = Array.isArray(directoryHeader) ? directoryHeader[0] : directoryHeader;
|
|
178
|
+
const directoryEncodingHeader = req.headers["x-opencode-directory-encoding"];
|
|
179
|
+
const directoryEncoding = Array.isArray(directoryEncodingHeader)
|
|
180
|
+
? directoryEncodingHeader[0]
|
|
181
|
+
: directoryEncodingHeader;
|
|
182
|
+
let headerDirectory = encodedDirectory;
|
|
183
|
+
if (encodedDirectory !== undefined &&
|
|
184
|
+
(directoryEncoding?.toLowerCase() === "uri" || /^(?:%|[a-z]%3a)/i.test(encodedDirectory))) {
|
|
185
|
+
headerDirectory = decodeURIComponent(encodedDirectory);
|
|
186
|
+
}
|
|
187
|
+
const directory = directoryQuery ?? headerDirectory ?? this.options.defaultDirectory;
|
|
90
188
|
// SSE stream (v2 clients subscribe at /api/event; v1 at /event)
|
|
91
189
|
if (method === "GET" && (path === "/event" || path === "/global/event")) {
|
|
92
|
-
this.handleSSE(res, "v1");
|
|
190
|
+
this.handleSSE(req, res, "v1", directory, path === "/global/event");
|
|
93
191
|
return;
|
|
94
192
|
}
|
|
95
193
|
if (method === "GET" && path === "/api/event") {
|
|
96
|
-
|
|
194
|
+
// OpenChamber 2.3.2 opens one unscoped /api/event as its global hub (captured);
|
|
195
|
+
// only an explicit directory selector scopes the stream.
|
|
196
|
+
this.handleSSE(req, res, "v2", directory, directoryQuery === null && headerDirectory === undefined);
|
|
97
197
|
return;
|
|
98
198
|
}
|
|
99
199
|
const services = {
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import type { OCPromptPart } from "./types.js";
|
|
1
2
|
import type { ManagedSession, SessionManager } from "./sessions.js";
|
|
2
3
|
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCToolPart, OCTextPart, OCUserMessage } from "./types.js";
|
|
3
|
-
|
|
4
|
+
type CommandDescriptor = {
|
|
4
5
|
name: string;
|
|
5
6
|
description: string;
|
|
6
7
|
template: "";
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
type SlashCommandInput = {
|
|
9
10
|
command: string;
|
|
10
11
|
arguments: string;
|
|
11
12
|
messageID?: string;
|
|
13
|
+
parts?: OCPromptPart[];
|
|
12
14
|
};
|
|
13
|
-
|
|
15
|
+
type ShellResult = {
|
|
14
16
|
output: string;
|
|
15
17
|
exitCode: number | null;
|
|
16
18
|
cancelled: boolean;
|
|
@@ -20,40 +22,36 @@ export type ShellMessage = OCMessageWithParts & {
|
|
|
20
22
|
info: OCAssistantMessage;
|
|
21
23
|
parts: [OCToolPart];
|
|
22
24
|
};
|
|
23
|
-
|
|
25
|
+
type ShellUserMessage = OCMessageWithParts & {
|
|
24
26
|
info: OCUserMessage;
|
|
25
27
|
parts: [OCTextPart];
|
|
26
28
|
};
|
|
27
|
-
|
|
28
|
-
info: OCAssistantMessage;
|
|
29
|
-
parts: [OCTextPart];
|
|
30
|
-
};
|
|
31
|
-
export type ShellExecution = {
|
|
29
|
+
type ShellExecution = {
|
|
32
30
|
kind: "completed";
|
|
33
31
|
message: ShellMessage;
|
|
34
32
|
} | {
|
|
35
33
|
kind: "error";
|
|
36
34
|
message: string;
|
|
37
35
|
};
|
|
38
|
-
|
|
36
|
+
interface ShellCommandContext {
|
|
39
37
|
sessions: SessionManager;
|
|
40
38
|
session: ManagedSession;
|
|
41
39
|
command: string;
|
|
42
40
|
emit: (directory: string, event: OCEvent) => void;
|
|
43
41
|
}
|
|
44
|
-
|
|
42
|
+
interface ShareNoticeContext {
|
|
45
43
|
sessions: SessionManager;
|
|
46
44
|
session: ManagedSession;
|
|
47
45
|
emit: (directory: string, event: OCEvent) => void;
|
|
48
46
|
}
|
|
49
|
-
|
|
47
|
+
type CompactResult = {
|
|
50
48
|
kind: "compacted";
|
|
51
49
|
} | {
|
|
52
50
|
kind: "error";
|
|
53
51
|
message: string;
|
|
54
52
|
};
|
|
55
53
|
export declare const unsupportedShare: {
|
|
56
|
-
readonly name: "
|
|
54
|
+
readonly name: "UnsupportedError";
|
|
57
55
|
readonly data: {
|
|
58
56
|
readonly message: "Sharing is not available with OmO";
|
|
59
57
|
};
|
|
@@ -69,10 +67,8 @@ export declare function parseSlashCommand(value: unknown): SlashCommandInput | u
|
|
|
69
67
|
export declare function slashTitlePrompt(text: string): {
|
|
70
68
|
sessionTitlePrompt?: string;
|
|
71
69
|
};
|
|
72
|
-
export declare function slashCommandPrompt(input: SlashCommandInput): string;
|
|
73
70
|
export declare function runSlashCommand(sessions: SessionManager, sessionId: string, input: SlashCommandInput): Promise<OCMessageWithParts | undefined>;
|
|
74
71
|
export declare function parseShellCommand(value: unknown): string | undefined;
|
|
75
|
-
export declare function parseShellResult(value: unknown): ShellResult;
|
|
76
72
|
export declare function createShellMessage(session: ManagedSession, command: string): ShellMessage;
|
|
77
73
|
export declare function shellUserMessage(message: ShellMessage): ShellUserMessage;
|
|
78
74
|
export declare function announceSessionNotice(context: ShareNoticeContext, messageText: string, error?: OCAssistantMessage["error"]): void;
|
|
@@ -81,3 +77,4 @@ export declare function completeShellMessage(message: ShellMessage, command: str
|
|
|
81
77
|
export declare function failShellMessage(message: ShellMessage, command: string, error: string, metadata?: Record<string, unknown>): void;
|
|
82
78
|
export declare function compactSession(sessions: SessionManager, sessionId: string): Promise<CompactResult>;
|
|
83
79
|
export declare function runShellCommand(context: ShellCommandContext): Promise<ShellExecution>;
|
|
80
|
+
export {};
|
package/dist/session-commands.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { PiRpcClient } from "./pi-rpc.js";
|
|
2
|
+
import { PiRpcClient, UpstreamProtocolError } from "./pi-rpc.js";
|
|
3
3
|
import { getAgentName } from "./agent-name.js";
|
|
4
|
+
import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
|
|
4
5
|
// OpenChamber drops rejected Share actions, so emit a visible session notice too.
|
|
5
6
|
export const unsupportedShare = {
|
|
6
|
-
name: "
|
|
7
|
+
name: "UnsupportedError",
|
|
7
8
|
data: { message: "Sharing is not available with OmO" },
|
|
8
9
|
};
|
|
9
10
|
function isRecord(value) {
|
|
@@ -12,8 +13,10 @@ function isRecord(value) {
|
|
|
12
13
|
function id(prefix) {
|
|
13
14
|
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
14
15
|
}
|
|
16
|
+
const SHELL_TITLE_MAX_CHARS = 60;
|
|
17
|
+
const SHELL_TITLE_TRUNCATED_CHARS = 57;
|
|
15
18
|
export function shellTitle(command) {
|
|
16
|
-
const short = command.length >
|
|
19
|
+
const short = command.length > SHELL_TITLE_MAX_CHARS ? `${command.slice(0, SHELL_TITLE_TRUNCATED_CHARS)}…` : command;
|
|
17
20
|
return `bash: ${short}`;
|
|
18
21
|
}
|
|
19
22
|
function assistantMessage(session) {
|
|
@@ -21,7 +24,7 @@ function assistantMessage(session) {
|
|
|
21
24
|
const messages = session.messages ?? [];
|
|
22
25
|
const boundary = messages.findIndex((message) => message.info.id === session.tree.revert?.messageID);
|
|
23
26
|
const visible = boundary < 0 ? messages : messages.slice(0, boundary);
|
|
24
|
-
const parentID =
|
|
27
|
+
const parentID = visible.findLast((message) => message.info.role === "user")?.info.id ?? "";
|
|
25
28
|
return {
|
|
26
29
|
id: id("msg"),
|
|
27
30
|
sessionID: session.info.id,
|
|
@@ -112,7 +115,14 @@ export function parseSlashCommand(value) {
|
|
|
112
115
|
return undefined;
|
|
113
116
|
// OpenChamber inserts the command's user message optimistically under this id.
|
|
114
117
|
const messageID = typeof value.messageID === "string" && value.messageID !== "" ? value.messageID : undefined;
|
|
115
|
-
|
|
118
|
+
const parts = parsePromptParts(value.parts);
|
|
119
|
+
nativePromptInput("", parts);
|
|
120
|
+
return {
|
|
121
|
+
command: value.command,
|
|
122
|
+
arguments: value.arguments,
|
|
123
|
+
...(messageID ? { messageID } : {}),
|
|
124
|
+
...(parts.length > 0 ? { parts } : {}),
|
|
125
|
+
};
|
|
116
126
|
}
|
|
117
127
|
/**
|
|
118
128
|
* OmO never auto-titles a first prompt that starts with "/", so a session opened
|
|
@@ -123,21 +133,22 @@ export function slashTitlePrompt(text) {
|
|
|
123
133
|
const match = /^\s*\/\S+\s+([\s\S]*\S)/.exec(text);
|
|
124
134
|
return match?.[1] !== undefined && !match[1].startsWith("/") ? { sessionTitlePrompt: match[1] } : {};
|
|
125
135
|
}
|
|
126
|
-
|
|
136
|
+
function slashCommandPrompt(input) {
|
|
127
137
|
return input.arguments === "" ? `/${input.command}` : `/${input.command} ${input.arguments}`;
|
|
128
138
|
}
|
|
129
139
|
export async function runSlashCommand(sessions, sessionId, input) {
|
|
130
|
-
|
|
140
|
+
const text = slashCommandPrompt(input);
|
|
141
|
+
await sessions.prompt(sessionId, text, undefined, false, input.messageID, "steer", input.parts ? [{ type: "text", text }, ...input.parts] : undefined);
|
|
131
142
|
await sessions.reloadMessages(sessionId);
|
|
132
143
|
const messages = sessions.messages(sessionId) ?? [];
|
|
133
|
-
return
|
|
144
|
+
return messages.findLast((message) => message.info.role === "assistant") ?? messages[messages.length - 1];
|
|
134
145
|
}
|
|
135
146
|
export function parseShellCommand(value) {
|
|
136
147
|
if (!isRecord(value) || typeof value.command !== "string" || value.command.trim() === "")
|
|
137
148
|
return undefined;
|
|
138
149
|
return value.command;
|
|
139
150
|
}
|
|
140
|
-
|
|
151
|
+
function parseShellResult(value) {
|
|
141
152
|
if (!isRecord(value) ||
|
|
142
153
|
typeof value.output !== "string" ||
|
|
143
154
|
(typeof value.exitCode !== "number" &&
|
|
@@ -145,7 +156,7 @@ export function parseShellResult(value) {
|
|
|
145
156
|
!(value.cancelled === true && value.exitCode === undefined)) ||
|
|
146
157
|
typeof value.cancelled !== "boolean" ||
|
|
147
158
|
typeof value.truncated !== "boolean") {
|
|
148
|
-
throw new
|
|
159
|
+
throw new UpstreamProtocolError("OmO bash returned an invalid result");
|
|
149
160
|
}
|
|
150
161
|
return {
|
|
151
162
|
output: value.output,
|
|
@@ -265,6 +276,15 @@ export async function runShellCommand(context) {
|
|
|
265
276
|
sessions.messages(session.info.id);
|
|
266
277
|
const message = createShellMessage(session, command);
|
|
267
278
|
const user = shellUserMessage(message);
|
|
279
|
+
const shell = {
|
|
280
|
+
id: message.parts[0].callID,
|
|
281
|
+
command,
|
|
282
|
+
cwd: session.info.directory,
|
|
283
|
+
shell: "bash",
|
|
284
|
+
file: "",
|
|
285
|
+
metadata: {},
|
|
286
|
+
time: { started: message.info.time.created },
|
|
287
|
+
};
|
|
268
288
|
const dispatch = (session.tree.shellDispatch ??= {});
|
|
269
289
|
dispatch[message.info.id] = false;
|
|
270
290
|
const afterEntryId = session.tree.leafId ?? null;
|
|
@@ -285,8 +305,12 @@ export async function runShellCommand(context) {
|
|
|
285
305
|
shellEvent(context, { type: "message.updated", properties: { info: user.info } });
|
|
286
306
|
shellEvent(context, { type: "message.part.updated", properties: { part: user.parts[0] } });
|
|
287
307
|
emitShellMessage(context, message);
|
|
308
|
+
shellEvent(context, {
|
|
309
|
+
type: "session.shell.started",
|
|
310
|
+
properties: { sessionID: session.info.id, shell: { ...shell, status: "running" } },
|
|
311
|
+
});
|
|
288
312
|
try {
|
|
289
|
-
sessions.update(session.info.id, {});
|
|
313
|
+
await sessions.update(session.info.id, {});
|
|
290
314
|
const response = await sessions.sessionRpc(session.info.id, { type: "bash", command }, controller.signal, () => {
|
|
291
315
|
dispatch[message.info.id] = true;
|
|
292
316
|
// Only actual dispatch admissions are durable; queued attempts stay in the live cache.
|
|
@@ -294,7 +318,7 @@ export async function runShellCommand(context) {
|
|
|
294
318
|
// A native result can land beyond the last synchronized leaf.
|
|
295
319
|
if (!session.tree.revert)
|
|
296
320
|
delete session.tree.leafId;
|
|
297
|
-
sessions.update(session.info.id, {});
|
|
321
|
+
void sessions.update(session.info.id, {}).catch((error) => controller.abort(error));
|
|
298
322
|
});
|
|
299
323
|
if (controller.signal.aborted)
|
|
300
324
|
throw controller.signal.reason;
|
|
@@ -326,7 +350,24 @@ export async function runShellCommand(context) {
|
|
|
326
350
|
}
|
|
327
351
|
finally {
|
|
328
352
|
delete session.cancelShell;
|
|
353
|
+
const state = message.parts[0].state;
|
|
354
|
+
const metadata = state.status === "pending" ? undefined : state.metadata;
|
|
355
|
+
const output = state.status === "completed" ? state.output : state.status === "error" ? state.error : "";
|
|
356
|
+
const size = Buffer.byteLength(output);
|
|
357
|
+
shellEvent(context, {
|
|
358
|
+
type: "session.shell.ended",
|
|
359
|
+
properties: {
|
|
360
|
+
sessionID: session.info.id,
|
|
361
|
+
shell: {
|
|
362
|
+
...shell,
|
|
363
|
+
status: state.status === "completed" ? "exited" : "killed",
|
|
364
|
+
...(typeof metadata?.exitCode === "number" ? { exit: metadata.exitCode } : {}),
|
|
365
|
+
time: { ...shell.time, completed: message.info.time.completed ?? Date.now() },
|
|
366
|
+
},
|
|
367
|
+
output: { output, cursor: size, size, truncated: metadata?.truncated === true },
|
|
368
|
+
},
|
|
369
|
+
});
|
|
329
370
|
setShellStatus(context, "idle");
|
|
330
|
-
sessions.update(session.info.id, {});
|
|
371
|
+
await sessions.update(session.info.id, {});
|
|
331
372
|
}
|
|
332
373
|
}
|
package/dist/session-diff.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface SessionFileDiff {
|
|
|
11
11
|
status: "added" | "modified" | "deleted";
|
|
12
12
|
}
|
|
13
13
|
export declare function touchedFiles(parts: readonly OCPart[]): string[];
|
|
14
|
-
export declare function sessionDiff(directory: string, messages: readonly OCMessageWithParts[]): Promise<SessionFileDiff[]>;
|
|
14
|
+
export declare function sessionDiff(directory: string, messages: readonly OCMessageWithParts[], context?: number): Promise<SessionFileDiff[]>;
|
package/dist/session-diff.js
CHANGED
|
@@ -3,6 +3,8 @@ import { readFile, realpath, lstat } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
const exec = promisify(execFile);
|
|
6
|
+
const GIT_EXEC_TIMEOUT_MS = 5_000;
|
|
7
|
+
const GIT_EXEC_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
6
8
|
export function touchedFiles(parts) {
|
|
7
9
|
return [
|
|
8
10
|
...new Set(parts.flatMap((part) => part.type === "tool" &&
|
|
@@ -14,7 +16,11 @@ export function touchedFiles(parts) {
|
|
|
14
16
|
];
|
|
15
17
|
}
|
|
16
18
|
async function git(directory, args) {
|
|
17
|
-
const result = await exec("git", args, {
|
|
19
|
+
const result = await exec("git", args, {
|
|
20
|
+
cwd: directory,
|
|
21
|
+
timeout: GIT_EXEC_TIMEOUT_MS,
|
|
22
|
+
maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
|
|
23
|
+
});
|
|
18
24
|
return result.stdout;
|
|
19
25
|
}
|
|
20
26
|
/** A removed file may be absent, but neither traversal nor symlinks may escape. */
|
|
@@ -49,7 +55,7 @@ async function confinedFile(directory, path) {
|
|
|
49
55
|
}
|
|
50
56
|
return absolute;
|
|
51
57
|
}
|
|
52
|
-
export async function sessionDiff(directory, messages) {
|
|
58
|
+
export async function sessionDiff(directory, messages, context = 3) {
|
|
53
59
|
let gitRoot;
|
|
54
60
|
try {
|
|
55
61
|
gitRoot = (await git(directory, ["rev-parse", "--show-toplevel"])).trim();
|
|
@@ -104,6 +110,7 @@ export async function sessionDiff(directory, messages) {
|
|
|
104
110
|
"HEAD",
|
|
105
111
|
"--no-ext-diff",
|
|
106
112
|
"--no-color",
|
|
113
|
+
`--unified=${Math.floor(context)}`,
|
|
107
114
|
"--src-prefix=a/",
|
|
108
115
|
"--dst-prefix=b/",
|
|
109
116
|
"--",
|
|
@@ -118,6 +125,7 @@ export async function sessionDiff(directory, messages) {
|
|
|
118
125
|
"--no-index",
|
|
119
126
|
"--no-ext-diff",
|
|
120
127
|
"--no-color",
|
|
128
|
+
`--unified=${Math.floor(context)}`,
|
|
121
129
|
"--src-prefix=a/",
|
|
122
130
|
"--dst-prefix=b/",
|
|
123
131
|
"--",
|
|
@@ -5,7 +5,7 @@ import { type PiUsage } from "./pi-event-translation.js";
|
|
|
5
5
|
export interface PiFileMessage {
|
|
6
6
|
clientMessageId?: string;
|
|
7
7
|
role?: string;
|
|
8
|
-
content?: Array<Record<string, unknown>>;
|
|
8
|
+
content?: string | Array<Record<string, unknown>>;
|
|
9
9
|
usage?: PiUsage;
|
|
10
10
|
provider?: string;
|
|
11
11
|
model?: string;
|