@omercnet/paseo-omp 0.2.1 → 0.3.0-next.91.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 +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +6 -2
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +213 -9
- package/server/provider/host-tools.ts +71 -0
- package/server/provider/omp-rpc.ts +82 -15
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/session-descriptors.ts +306 -1
- package/server/provider/session.ts +704 -249
- package/server/provider/subsessions.ts +4 -1
- package/server/provider/timeline-projector.ts +70 -33
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -304,6 +304,7 @@ export class OmpSubsessionProjector {
|
|
|
304
304
|
private readonly scheduler: OmpTimelineScheduler,
|
|
305
305
|
private readonly onActivityChange: () => void,
|
|
306
306
|
private readonly outputRedactionValues: readonly string[],
|
|
307
|
+
private readonly pluginTimelineEnabled = false,
|
|
307
308
|
) {
|
|
308
309
|
this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
|
|
309
310
|
}
|
|
@@ -540,6 +541,8 @@ export class OmpSubsessionProjector {
|
|
|
540
541
|
this.emit,
|
|
541
542
|
this.scheduler,
|
|
542
543
|
this.outputRedactionValues,
|
|
544
|
+
false,
|
|
545
|
+
this.pluginTimelineEnabled,
|
|
543
546
|
),
|
|
544
547
|
};
|
|
545
548
|
this.children.set(sessionId, child);
|
|
@@ -552,7 +555,7 @@ export class OmpSubsessionProjector {
|
|
|
552
555
|
type: "session.opened",
|
|
553
556
|
sessionId,
|
|
554
557
|
parentSessionId,
|
|
555
|
-
capabilities: [],
|
|
558
|
+
capabilities: ["session.subsession"],
|
|
556
559
|
restoration: "parent",
|
|
557
560
|
cwd: this.cwd,
|
|
558
561
|
title,
|
|
@@ -4,8 +4,9 @@ import type {
|
|
|
4
4
|
ProviderTimelineItem,
|
|
5
5
|
ProviderToolCallDetail,
|
|
6
6
|
} from "@getpaseo/plugin/server/provider";
|
|
7
|
+
import { OMP_MCP_AUTH_TIMELINE_KIND } from "../../shared/mcp";
|
|
7
8
|
import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
|
|
8
|
-
import type
|
|
9
|
+
import { OMP_MAX_CONTENT_PARTS, type OmpMessage, type OmpRpcEvent } from "./omp-rpc";
|
|
9
10
|
import {
|
|
10
11
|
boundedJsonBytes,
|
|
11
12
|
type JsonValue,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
} from "./security";
|
|
16
17
|
|
|
17
18
|
const STREAM_FRAME_MS = 32;
|
|
18
|
-
const MAX_STREAM_CONTENT_BLOCKS = 64;
|
|
19
19
|
const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
|
|
20
20
|
const MAX_ACTIVE_TOOLS = 64;
|
|
21
21
|
const MAX_TODOS = 256;
|
|
@@ -54,6 +54,7 @@ type StreamSnapshot = {
|
|
|
54
54
|
nativeIdentity?: string;
|
|
55
55
|
published: boolean;
|
|
56
56
|
retainedBytes: number;
|
|
57
|
+
textBytes: number;
|
|
57
58
|
publishedBytes: number;
|
|
58
59
|
blocks: Map<number, StreamBlockSnapshot>;
|
|
59
60
|
dirtyBlocks: Set<number>;
|
|
@@ -196,6 +197,17 @@ function publishableHttpUrl(value: string | undefined): string | undefined {
|
|
|
196
197
|
return undefined;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
function isOmpOAuthLaunchUrl(value: string | undefined): boolean {
|
|
201
|
+
const url = publishableHttpUrl(value);
|
|
202
|
+
if (!url) return false;
|
|
203
|
+
const parsed = new URL(url);
|
|
204
|
+
return (
|
|
205
|
+
(parsed.hostname === "localhost" ||
|
|
206
|
+
parsed.hostname === "127.0.0.1" ||
|
|
207
|
+
parsed.hostname === "[::1]") &&
|
|
208
|
+
parsed.pathname === "/launch"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
199
211
|
|
|
200
212
|
function todoPublicId(nativeId: string | undefined, index: number): string {
|
|
201
213
|
if (!nativeId) return `omp:todo:${index}`;
|
|
@@ -240,7 +252,7 @@ function nativeImageResult(
|
|
|
240
252
|
boundedJsonBytes(
|
|
241
253
|
value,
|
|
242
254
|
MAX_NATIVE_IMAGE_RESULT_BYTES,
|
|
243
|
-
|
|
255
|
+
OMP_MAX_CONTENT_PARTS,
|
|
244
256
|
8 * 1024 * 1024,
|
|
245
257
|
512,
|
|
246
258
|
) === Number.POSITIVE_INFINITY
|
|
@@ -350,6 +362,7 @@ export class OmpTimelineProjector {
|
|
|
350
362
|
private readonly replayCandidates = new Map<string, ReplayCandidate>();
|
|
351
363
|
private readonly replayOverflowCandidates = new Map<string, string>();
|
|
352
364
|
private projectingReplay = false;
|
|
365
|
+
private readonly replayToolCalls = new Map<string, { toolName: string; args: unknown }>();
|
|
353
366
|
private customSequence = 0;
|
|
354
367
|
private compactionSequence = 0;
|
|
355
368
|
private activeCompaction: CompactionSlot | null = null;
|
|
@@ -366,6 +379,7 @@ export class OmpTimelineProjector {
|
|
|
366
379
|
private closed = false;
|
|
367
380
|
|
|
368
381
|
private readonly dataFilter: OmpPublicDataSerializer;
|
|
382
|
+
private browserAuthorizationIssuer: ((url: string) => string | undefined) | null = null;
|
|
369
383
|
|
|
370
384
|
constructor(
|
|
371
385
|
private readonly sessionId: string,
|
|
@@ -373,10 +387,14 @@ export class OmpTimelineProjector {
|
|
|
373
387
|
private readonly scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
|
|
374
388
|
outputRedactionValues: readonly string[] = [],
|
|
375
389
|
private readonly conversationRevertEnabled = false,
|
|
390
|
+
private readonly pluginTimelineEnabled = false,
|
|
376
391
|
private readonly hostToolLabels: ReadonlyMap<string, string> = new Map(),
|
|
377
392
|
) {
|
|
378
393
|
this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
|
|
379
394
|
}
|
|
395
|
+
setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
|
|
396
|
+
this.browserAuthorizationIssuer = issue;
|
|
397
|
+
}
|
|
380
398
|
|
|
381
399
|
project(event: OmpRpcEvent, turnId: string, bypassReplayFilter = false): void {
|
|
382
400
|
if (this.closed) return;
|
|
@@ -601,15 +619,40 @@ export class OmpTimelineProjector {
|
|
|
601
619
|
return;
|
|
602
620
|
}
|
|
603
621
|
if (event.type === "extension_ui_request" && event.method === "open_url") {
|
|
604
|
-
const url = publishableHttpUrl(event.
|
|
622
|
+
const url = publishableHttpUrl(event.url);
|
|
605
623
|
if (!url) return;
|
|
606
624
|
this.noticeSequence += 1;
|
|
607
|
-
const
|
|
625
|
+
const instructions = event.instructions
|
|
626
|
+
? this.dataFilter.text(event.instructions, 64 * 1024)
|
|
627
|
+
: undefined;
|
|
628
|
+
const publicUrl = this.dataFilter.text(url, 16_384);
|
|
629
|
+
const isMcpAuthorization = isOmpOAuthLaunchUrl(event.launchUrl);
|
|
630
|
+
if (this.pluginTimelineEnabled && isMcpAuthorization && publishableHttpUrl(publicUrl)) {
|
|
631
|
+
const browserAuthorizationToken = this.browserAuthorizationIssuer?.(publicUrl);
|
|
632
|
+
this.publish({
|
|
633
|
+
type: "plugin",
|
|
634
|
+
id: `omp:ui:${this.noticeSequence}`,
|
|
635
|
+
pluginId: "paseo-omp",
|
|
636
|
+
kind: OMP_MCP_AUTH_TIMELINE_KIND,
|
|
637
|
+
version: 1,
|
|
638
|
+
data: {
|
|
639
|
+
url: publicUrl,
|
|
640
|
+
...(instructions ? { instructions } : {}),
|
|
641
|
+
loopbackCallback: true,
|
|
642
|
+
...(browserAuthorizationToken ? { browserAuthorizationToken } : {}),
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const remoteHint = isMcpAuthorization
|
|
648
|
+
? "Remote client: if the localhost callback cannot connect, paste the final redirect URL or authorization code into the OMP prompt in this chat."
|
|
649
|
+
: undefined;
|
|
650
|
+
const message = [instructions, publicUrl, remoteHint].filter(Boolean).join("\n");
|
|
608
651
|
this.publish({
|
|
609
652
|
type: "notification",
|
|
610
653
|
id: `omp:ui:${this.noticeSequence}`,
|
|
611
654
|
level: "info",
|
|
612
|
-
message
|
|
655
|
+
message,
|
|
613
656
|
});
|
|
614
657
|
return;
|
|
615
658
|
}
|
|
@@ -783,6 +826,7 @@ export class OmpTimelineProjector {
|
|
|
783
826
|
this.replayOverflowCandidates.clear();
|
|
784
827
|
this.projectingReplay = false;
|
|
785
828
|
this.activeToolBytes = 0;
|
|
829
|
+
this.replayToolCalls.clear();
|
|
786
830
|
this.tools.clear();
|
|
787
831
|
this.commandText = "";
|
|
788
832
|
this.commandPublishedText = "";
|
|
@@ -795,6 +839,7 @@ export class OmpTimelineProjector {
|
|
|
795
839
|
const nativeIdentity = assistantIdentity(message);
|
|
796
840
|
this.replaySequence += 1;
|
|
797
841
|
if (message.role === "user") {
|
|
842
|
+
this.replayToolCalls.clear();
|
|
798
843
|
if (this.replayTurnId) this.finishTurn(this.replayTurnId);
|
|
799
844
|
this.replayTurnId = `omp:replay-turn:${this.replaySequence}`;
|
|
800
845
|
const text =
|
|
@@ -823,15 +868,7 @@ export class OmpTimelineProjector {
|
|
|
823
868
|
for (const part of message.content) {
|
|
824
869
|
if (part.type !== "toolCall" || !part.id || !part.name || part.arguments === undefined)
|
|
825
870
|
continue;
|
|
826
|
-
this.
|
|
827
|
-
{
|
|
828
|
-
type: "tool_execution_start",
|
|
829
|
-
toolCallId: part.id,
|
|
830
|
-
toolName: part.name,
|
|
831
|
-
args: part.arguments,
|
|
832
|
-
},
|
|
833
|
-
this.replayTurnId,
|
|
834
|
-
);
|
|
871
|
+
this.replayToolCalls.set(part.id, { toolName: part.name, args: part.arguments });
|
|
835
872
|
}
|
|
836
873
|
}
|
|
837
874
|
} finally {
|
|
@@ -842,13 +879,15 @@ export class OmpTimelineProjector {
|
|
|
842
879
|
}
|
|
843
880
|
this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
|
|
844
881
|
if (message.role === "toolResult") {
|
|
882
|
+
const replayCall = this.replayToolCalls.get(message.toolCallId);
|
|
883
|
+
this.replayToolCalls.delete(message.toolCallId);
|
|
845
884
|
if (!this.tools.has(message.toolCallId)) {
|
|
846
885
|
this.project(
|
|
847
886
|
{
|
|
848
887
|
type: "tool_execution_start",
|
|
849
888
|
toolCallId: message.toolCallId,
|
|
850
|
-
toolName: message.toolName,
|
|
851
|
-
args: null,
|
|
889
|
+
toolName: replayCall?.toolName ?? message.toolName,
|
|
890
|
+
args: replayCall?.args ?? null,
|
|
852
891
|
},
|
|
853
892
|
this.replayTurnId,
|
|
854
893
|
);
|
|
@@ -880,6 +919,7 @@ export class OmpTimelineProjector {
|
|
|
880
919
|
|
|
881
920
|
finishReplay(): void {
|
|
882
921
|
if (this.replayTurnId) this.finishTurn(this.replayTurnId);
|
|
922
|
+
this.replayToolCalls.clear();
|
|
883
923
|
this.replayTurnId = null;
|
|
884
924
|
}
|
|
885
925
|
acceptLiveTurn(turnId: string): void {
|
|
@@ -1027,9 +1067,11 @@ export class OmpTimelineProjector {
|
|
|
1027
1067
|
if (this.currentTurnId) this.publishCommand(this.currentTurnId);
|
|
1028
1068
|
this.retireCompactions("OMP compaction ended when the session closed");
|
|
1029
1069
|
this.closed = true;
|
|
1070
|
+
this.browserAuthorizationIssuer = null;
|
|
1030
1071
|
this.clearFlushTimer();
|
|
1031
1072
|
this.stream = null;
|
|
1032
1073
|
this.tools.clear();
|
|
1074
|
+
this.replayToolCalls.clear();
|
|
1033
1075
|
this.activeToolBytes = 0;
|
|
1034
1076
|
this.replayCandidates.clear();
|
|
1035
1077
|
this.replayOverflowCandidates.clear();
|
|
@@ -1091,6 +1133,7 @@ export class OmpTimelineProjector {
|
|
|
1091
1133
|
...(nativeIdentity ? { nativeIdentity } : {}),
|
|
1092
1134
|
published: false,
|
|
1093
1135
|
retainedBytes: 0,
|
|
1136
|
+
textBytes: 0,
|
|
1094
1137
|
publishedBytes: 0,
|
|
1095
1138
|
blocks: new Map(),
|
|
1096
1139
|
dirtyBlocks: new Set(),
|
|
@@ -1155,7 +1198,7 @@ export class OmpTimelineProjector {
|
|
|
1155
1198
|
return;
|
|
1156
1199
|
}
|
|
1157
1200
|
if (!Array.isArray(message.content)) return;
|
|
1158
|
-
const blockCount = Math.min(message.content.length,
|
|
1201
|
+
const blockCount = Math.min(message.content.length, OMP_MAX_CONTENT_PARTS);
|
|
1159
1202
|
for (let index = 0; index < blockCount; index += 1) {
|
|
1160
1203
|
const block = blockText(message, index);
|
|
1161
1204
|
if (block) this.setBlock(stream, index, block);
|
|
@@ -1215,20 +1258,13 @@ export class OmpTimelineProjector {
|
|
|
1215
1258
|
if (snapshot.kind === "image" && utf8Bytes(snapshot.text) > MAX_IMAGE_ENCODED_LENGTH + 256)
|
|
1216
1259
|
return;
|
|
1217
1260
|
const previous = stream.blocks.get(contentIndex);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (
|
|
1226
|
-
retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
|
|
1227
|
-
textBytes > MAX_STREAM_TEXT_LENGTH
|
|
1228
|
-
) {
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1261
|
+
const previousBytes = previous ? utf8Bytes(previous.text) : 0;
|
|
1262
|
+
const snapshotBytes = utf8Bytes(snapshot.text);
|
|
1263
|
+
const retainedBytes = stream.retainedBytes - previousBytes + snapshotBytes;
|
|
1264
|
+
const textBytes =
|
|
1265
|
+
stream.textBytes -
|
|
1266
|
+
(previous && previous.kind !== "image" ? previousBytes : 0) +
|
|
1267
|
+
(snapshot.kind === "image" ? 0 : snapshotBytes);
|
|
1232
1268
|
if (
|
|
1233
1269
|
retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
|
|
1234
1270
|
textBytes > MAX_STREAM_TEXT_LENGTH
|
|
@@ -1237,6 +1273,7 @@ export class OmpTimelineProjector {
|
|
|
1237
1273
|
}
|
|
1238
1274
|
if (previous?.kind === snapshot.kind && previous.text === snapshot.text) return;
|
|
1239
1275
|
stream.retainedBytes = retainedBytes;
|
|
1276
|
+
stream.textBytes = textBytes;
|
|
1240
1277
|
stream.blocks.set(contentIndex, {
|
|
1241
1278
|
...snapshot,
|
|
1242
1279
|
...(previous?.kind === snapshot.kind ? { publishedText: previous.publishedText } : {}),
|
|
@@ -1248,7 +1285,7 @@ export class OmpTimelineProjector {
|
|
|
1248
1285
|
return (
|
|
1249
1286
|
Number.isSafeInteger(contentIndex) &&
|
|
1250
1287
|
contentIndex >= 0 &&
|
|
1251
|
-
contentIndex <
|
|
1288
|
+
contentIndex < OMP_MAX_CONTENT_PARTS
|
|
1252
1289
|
);
|
|
1253
1290
|
}
|
|
1254
1291
|
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
OmpVersionStatus,
|
|
18
18
|
PathState,
|
|
19
19
|
} from "../shared/provider-diagnostics";
|
|
20
|
-
import { ompAgentDir } from "./paths";
|
|
20
|
+
import { currentOmpEnvironment, ompAgentDir, ompDataDir, ompSessionDir } from "./paths";
|
|
21
21
|
|
|
22
22
|
const VERSION_TIMEOUT_MS = 3_000;
|
|
23
23
|
const HELP_TIMEOUT_MS = 3_000;
|
|
@@ -190,7 +190,11 @@ export async function killWindowsProcessTree(
|
|
|
190
190
|
|
|
191
191
|
let child: ProbeChildProcess;
|
|
192
192
|
try {
|
|
193
|
-
child = spawnFn(
|
|
193
|
+
child = spawnFn(
|
|
194
|
+
taskkillPath,
|
|
195
|
+
["/pid", String(pid), "/t", "/f"],
|
|
196
|
+
buildProbeEnv(currentOmpEnvironment()),
|
|
197
|
+
);
|
|
194
198
|
} catch {
|
|
195
199
|
return false;
|
|
196
200
|
}
|
|
@@ -243,7 +247,7 @@ export function defaultSpawn(
|
|
|
243
247
|
return killWindowsProcessTree(
|
|
244
248
|
child.pid,
|
|
245
249
|
defaultSpawn,
|
|
246
|
-
|
|
250
|
+
currentOmpEnvironment().SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT,
|
|
247
251
|
graceMs,
|
|
248
252
|
);
|
|
249
253
|
}
|
|
@@ -569,7 +573,7 @@ export async function probeOmpAvailability(options: OmpAvailabilityProbeOptions)
|
|
|
569
573
|
status: "missing" | "unrunnable" | "incompatible" | "available";
|
|
570
574
|
diagnostic?: string;
|
|
571
575
|
}> {
|
|
572
|
-
const environment = options.environment ??
|
|
576
|
+
const environment = options.environment ?? currentOmpEnvironment();
|
|
573
577
|
const [command, ...prefixArgs] = options.command;
|
|
574
578
|
const resolvedPath = await resolveExecutablePath(
|
|
575
579
|
command,
|
|
@@ -810,31 +814,57 @@ async function computeMcpDiagnostics(agentDir: string): Promise<OmpMcpDiagnostic
|
|
|
810
814
|
export interface ProcessDiagnosticsFs {
|
|
811
815
|
readdir(path: string): Promise<string[]>;
|
|
812
816
|
lstat(path: string): Promise<Stats>;
|
|
817
|
+
readMetadata(path: string, maxBytes: number): Promise<BoundedFileRead>;
|
|
813
818
|
}
|
|
814
819
|
|
|
815
|
-
const processDiagnosticsFs: ProcessDiagnosticsFs = {
|
|
820
|
+
const processDiagnosticsFs: ProcessDiagnosticsFs = {
|
|
821
|
+
readdir,
|
|
822
|
+
lstat,
|
|
823
|
+
readMetadata: readBoundedNoSymlinkFile,
|
|
824
|
+
};
|
|
825
|
+
const MAX_PROCESS_METADATA_BYTES = 64 * 1024;
|
|
826
|
+
const MAX_PROCESS_METADATA_ENTRIES = 4_096;
|
|
827
|
+
const ProcessStateSchema = z.object({
|
|
828
|
+
daemon: z.object({
|
|
829
|
+
state: z.string().max(32),
|
|
830
|
+
exitedAt: z.number().finite().nonnegative().nullish(),
|
|
831
|
+
}),
|
|
832
|
+
});
|
|
833
|
+
const ACTIVE_PROCESS_STATES = new Set(["starting", "running", "ready", "restarting"]);
|
|
834
|
+
const HISTORICAL_PROCESS_STATES = new Set(["exited", "failed", "stopped"]);
|
|
816
835
|
|
|
817
836
|
/**
|
|
818
|
-
*
|
|
819
|
-
*
|
|
837
|
+
* Classifies bounded, regular, non-symlink Hub metadata. A recorded active state is not a
|
|
838
|
+
* liveness check. Historical files remain useful evidence and are never counted as running.
|
|
839
|
+
* Only aggregate counts leave this reader; metadata arguments, paths and values are discarded.
|
|
820
840
|
*/
|
|
821
841
|
export async function computeProcessDiagnostics(
|
|
822
842
|
hubRunRoot: string,
|
|
823
843
|
fs: ProcessDiagnosticsFs = processDiagnosticsFs,
|
|
824
844
|
): Promise<OmpProcessDiagnostics> {
|
|
845
|
+
const unavailableCounts = {
|
|
846
|
+
trackedCount: null,
|
|
847
|
+
activeCount: null,
|
|
848
|
+
historicalCount: null,
|
|
849
|
+
unknownCount: null,
|
|
850
|
+
};
|
|
825
851
|
let projectHashes: string[];
|
|
826
852
|
try {
|
|
827
853
|
const rootStats = await fs.lstat(hubRunRoot);
|
|
828
854
|
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
829
|
-
return { status: "unavailable",
|
|
855
|
+
return { status: "unavailable", ...unavailableCounts };
|
|
830
856
|
}
|
|
831
857
|
projectHashes = await fs.readdir(hubRunRoot);
|
|
832
858
|
} catch (error) {
|
|
833
|
-
return { status: isEnoent(error) ? "unavailable" : "unknown",
|
|
859
|
+
return { status: isEnoent(error) ? "unavailable" : "unknown", ...unavailableCounts };
|
|
834
860
|
}
|
|
835
861
|
let trackedCount = 0;
|
|
836
|
-
let
|
|
837
|
-
|
|
862
|
+
let activeCount = 0;
|
|
863
|
+
let historicalCount = 0;
|
|
864
|
+
let unknownCount = 0;
|
|
865
|
+
let partial = projectHashes.length > MAX_PROCESS_METADATA_ENTRIES;
|
|
866
|
+
let visitedEntries = 0;
|
|
867
|
+
for (const hash of projectHashes.slice(0, MAX_PROCESS_METADATA_ENTRIES)) {
|
|
838
868
|
const projectDir = join(hubRunRoot, hash);
|
|
839
869
|
const daemonsDir = join(projectDir, "daemons");
|
|
840
870
|
try {
|
|
@@ -855,15 +885,51 @@ export async function computeProcessDiagnostics(
|
|
|
855
885
|
continue;
|
|
856
886
|
}
|
|
857
887
|
for (const name of daemonNames) {
|
|
888
|
+
if (++visitedEntries > MAX_PROCESS_METADATA_ENTRIES) {
|
|
889
|
+
partial = true;
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
858
892
|
try {
|
|
859
|
-
const
|
|
860
|
-
|
|
893
|
+
const daemonDir = join(daemonsDir, name);
|
|
894
|
+
const directoryStats = await fs.lstat(daemonDir);
|
|
895
|
+
if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory()) continue;
|
|
896
|
+
const metaPath = join(daemonDir, "meta.json");
|
|
897
|
+
const metaStats = await fs.lstat(metaPath);
|
|
898
|
+
if (metaStats.isSymbolicLink() || !metaStats.isFile()) continue;
|
|
899
|
+
trackedCount += 1;
|
|
900
|
+
let category: "active" | "historical" | "unknown" = "unknown";
|
|
901
|
+
try {
|
|
902
|
+
const file = await fs.readMetadata(metaPath, MAX_PROCESS_METADATA_BYTES);
|
|
903
|
+
if (file.state === "available") {
|
|
904
|
+
const parsed = ProcessStateSchema.safeParse(JSON.parse(file.text));
|
|
905
|
+
if (parsed.success) {
|
|
906
|
+
const { state, exitedAt } = parsed.data.daemon;
|
|
907
|
+
if (HISTORICAL_PROCESS_STATES.has(state) || exitedAt != null) category = "historical";
|
|
908
|
+
else if (ACTIVE_PROCESS_STATES.has(state)) category = "active";
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
// Read/access/parse errors have an honest unknown bucket, never a guessed active state.
|
|
913
|
+
}
|
|
914
|
+
if (category === "historical") historicalCount += 1;
|
|
915
|
+
else if (category === "active") activeCount += 1;
|
|
916
|
+
else {
|
|
917
|
+
unknownCount += 1;
|
|
918
|
+
partial = true;
|
|
919
|
+
}
|
|
861
920
|
} catch (error) {
|
|
862
921
|
if (!isEnoent(error)) partial = true;
|
|
863
922
|
}
|
|
864
923
|
}
|
|
924
|
+
if (visitedEntries > MAX_PROCESS_METADATA_ENTRIES) break;
|
|
865
925
|
}
|
|
866
|
-
return {
|
|
926
|
+
return {
|
|
927
|
+
status: partial ? "partial" : "ok",
|
|
928
|
+
trackedCount,
|
|
929
|
+
activeCount,
|
|
930
|
+
historicalCount,
|
|
931
|
+
unknownCount,
|
|
932
|
+
};
|
|
867
933
|
}
|
|
868
934
|
|
|
869
935
|
function homeRelative(path: string, homeDir: string): string | null {
|
|
@@ -880,16 +946,23 @@ function sanitizeRootPath(path: string, homeDir: string): string {
|
|
|
880
946
|
return homeRelative(path, homeDir) ?? "<custom path>";
|
|
881
947
|
}
|
|
882
948
|
|
|
883
|
-
function sanitizeDerivedPath(
|
|
949
|
+
function sanitizeDerivedPath(
|
|
950
|
+
rawRoot: string,
|
|
951
|
+
sanitizedRoot: string,
|
|
952
|
+
fullPath: string,
|
|
953
|
+
fallbackHomeDir?: string,
|
|
954
|
+
): string {
|
|
884
955
|
const suffix = relative(rawRoot, fullPath);
|
|
885
956
|
if (suffix === "" || suffix === ".." || suffix.startsWith(`..${sep}`) || isAbsolute(suffix)) {
|
|
886
|
-
return sanitizedRoot;
|
|
957
|
+
return fallbackHomeDir ? sanitizeRootPath(fullPath, fallbackHomeDir) : sanitizedRoot;
|
|
887
958
|
}
|
|
888
959
|
return `${sanitizedRoot}/${suffix.split(sep).join("/")}`;
|
|
889
960
|
}
|
|
890
961
|
|
|
891
962
|
export interface ProviderDiagnosticsDeps {
|
|
892
963
|
agentDir: string;
|
|
964
|
+
dataDir?: string;
|
|
965
|
+
sessionDir?: string;
|
|
893
966
|
command: string;
|
|
894
967
|
pathDirs: readonly string[];
|
|
895
968
|
cwd: string;
|
|
@@ -910,6 +983,8 @@ export async function computeOmpProviderHealth(
|
|
|
910
983
|
deps: ProviderDiagnosticsDeps,
|
|
911
984
|
): Promise<OmpProviderHealth> {
|
|
912
985
|
const agentDir = resolve(deps.agentDir);
|
|
986
|
+
const dataDir = resolve(deps.dataDir ?? agentDir);
|
|
987
|
+
const sessionRoot = resolve(deps.sessionDir ?? join(dataDir, SESSION_DIR_NAME));
|
|
913
988
|
const homeDir = resolve(deps.homeDir);
|
|
914
989
|
const versionTimeoutMs = deps.versionTimeoutMs ?? VERSION_TIMEOUT_MS;
|
|
915
990
|
const helpTimeoutMs = deps.helpTimeoutMs ?? HELP_TIMEOUT_MS;
|
|
@@ -935,6 +1010,7 @@ export async function computeOmpProviderHealth(
|
|
|
935
1010
|
versionTimeoutMs,
|
|
936
1011
|
killGraceMs,
|
|
937
1012
|
maxVersionBytes,
|
|
1013
|
+
deps.cwd,
|
|
938
1014
|
)
|
|
939
1015
|
: null,
|
|
940
1016
|
installed
|
|
@@ -946,6 +1022,7 @@ export async function computeOmpProviderHealth(
|
|
|
946
1022
|
helpTimeoutMs,
|
|
947
1023
|
killGraceMs,
|
|
948
1024
|
maxHelpBytes,
|
|
1025
|
+
deps.cwd,
|
|
949
1026
|
)
|
|
950
1027
|
: null,
|
|
951
1028
|
]);
|
|
@@ -957,9 +1034,8 @@ export async function computeOmpProviderHealth(
|
|
|
957
1034
|
const lsp = helpRun ? computeLspDiagnostics(helpRun) : { status: "unknown" as const };
|
|
958
1035
|
const processCleanupFailed = Boolean(versionRun?.cleanupFailed || helpRun?.cleanupFailed);
|
|
959
1036
|
|
|
960
|
-
const agentDbPath = join(
|
|
961
|
-
const historyDbPath = join(
|
|
962
|
-
const sessionRoot = join(agentDir, SESSION_DIR_NAME);
|
|
1037
|
+
const agentDbPath = join(dataDir, AGENT_DB_FILENAME);
|
|
1038
|
+
const historyDbPath = join(dataDir, HISTORY_DB_FILENAME);
|
|
963
1039
|
const [
|
|
964
1040
|
configResult,
|
|
965
1041
|
agentRootState,
|
|
@@ -1000,7 +1076,7 @@ export async function computeOmpProviderHealth(
|
|
|
1000
1076
|
agentRootState,
|
|
1001
1077
|
configPath: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, configResult.path),
|
|
1002
1078
|
configState,
|
|
1003
|
-
sessionRoot: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, sessionRoot),
|
|
1079
|
+
sessionRoot: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, sessionRoot, homeDir),
|
|
1004
1080
|
sessionRootState,
|
|
1005
1081
|
},
|
|
1006
1082
|
databases: {
|
|
@@ -1013,11 +1089,11 @@ export async function computeOmpProviderHealth(
|
|
|
1013
1089
|
};
|
|
1014
1090
|
}
|
|
1015
1091
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1092
|
+
const cachedHealth = new Map<string, { value: OmpProviderHealth; expiresAt: number }>();
|
|
1093
|
+
const inFlightHealth = new Map<string, Promise<OmpProviderHealth>>();
|
|
1018
1094
|
|
|
1019
|
-
function defaultHubRunRoot(): string {
|
|
1020
|
-
return
|
|
1095
|
+
function defaultHubRunRoot(environment: NodeJS.ProcessEnv = currentOmpEnvironment()): string {
|
|
1096
|
+
return environment.PASEO_OMP_RUN_DIR ?? join(homedir(), ".omp", "run", "daemons");
|
|
1021
1097
|
}
|
|
1022
1098
|
|
|
1023
1099
|
/**
|
|
@@ -1029,29 +1105,39 @@ function defaultHubRunRoot(): string {
|
|
|
1029
1105
|
export async function resolveGetOmpProviderHealth(
|
|
1030
1106
|
input: RpcInput<typeof getOmpProviderHealth>,
|
|
1031
1107
|
): Promise<OmpProviderHealth> {
|
|
1108
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1109
|
+
const environment = currentOmpEnvironment();
|
|
1110
|
+
const agentDir = ompAgentDir(environment);
|
|
1111
|
+
const dataDir = ompDataDir(environment);
|
|
1112
|
+
const sessionDir = ompSessionDir(environment);
|
|
1032
1113
|
const now = Date.now();
|
|
1033
|
-
|
|
1034
|
-
|
|
1114
|
+
const key = JSON.stringify([cwd, agentDir, dataDir, sessionDir]);
|
|
1115
|
+
const cached = cachedHealth.get(key);
|
|
1116
|
+
if (!input.force && cached && cached.expiresAt > now) return cached.value;
|
|
1117
|
+
const inFlight = inFlightHealth.get(key);
|
|
1118
|
+
if (inFlight) return inFlight;
|
|
1035
1119
|
|
|
1036
1120
|
const computation = computeOmpProviderHealth({
|
|
1037
|
-
agentDir
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1121
|
+
agentDir,
|
|
1122
|
+
dataDir,
|
|
1123
|
+
sessionDir,
|
|
1124
|
+
command: environment.OMP_COMMAND ?? "omp",
|
|
1125
|
+
pathDirs: (environment.PATH ?? "").split(delimiter),
|
|
1126
|
+
cwd,
|
|
1041
1127
|
platform: process.platform,
|
|
1042
|
-
pathExt:
|
|
1043
|
-
env:
|
|
1128
|
+
pathExt: environment.PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
|
|
1129
|
+
env: environment,
|
|
1044
1130
|
spawnFn: defaultSpawn,
|
|
1045
|
-
hubRunRoot: defaultHubRunRoot(),
|
|
1131
|
+
hubRunRoot: defaultHubRunRoot(environment),
|
|
1046
1132
|
homeDir: homedir(),
|
|
1047
1133
|
})
|
|
1048
1134
|
.then((value) => {
|
|
1049
|
-
cachedHealth
|
|
1135
|
+
cachedHealth.set(key, { value, expiresAt: Date.now() + HEALTH_CACHE_TTL_MS });
|
|
1050
1136
|
return value;
|
|
1051
1137
|
})
|
|
1052
1138
|
.finally(() => {
|
|
1053
|
-
inFlightHealth
|
|
1139
|
+
inFlightHealth.delete(key);
|
|
1054
1140
|
});
|
|
1055
|
-
inFlightHealth
|
|
1141
|
+
inFlightHealth.set(key, computation);
|
|
1056
1142
|
return computation;
|
|
1057
1143
|
}
|
package/server/quota.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { DatabaseSync } from "node:sqlite";
|
|
2
3
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import type { listOmpQuotas, OmpQuota } from "../shared/quota";
|
|
5
|
-
import {
|
|
6
|
+
import { ompDataDir } from "./paths";
|
|
6
7
|
|
|
7
8
|
const QuotaRowSchema = z.object({
|
|
8
9
|
provider: z.string(),
|
|
@@ -50,5 +51,5 @@ export function listOmpQuotasFrom(path: string): OmpQuota[] {
|
|
|
50
51
|
export function resolveListOmpQuotas(_input: RpcInput<typeof listOmpQuotas>): {
|
|
51
52
|
quotas: OmpQuota[];
|
|
52
53
|
} {
|
|
53
|
-
return { quotas: listOmpQuotasFrom(
|
|
54
|
+
return { quotas: listOmpQuotasFrom(join(ompDataDir(), "agent.db")) };
|
|
54
55
|
}
|
package/server/sessions.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
3
3
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import type { listOmpSessions, OmpSessionEntry } from "../shared/sessions";
|
|
6
|
-
import {
|
|
6
|
+
import { ompDataDir } from "./paths";
|
|
7
7
|
|
|
8
8
|
const PROMPT_LIMIT = 400;
|
|
9
9
|
const ROW_LIMIT = 100;
|
|
@@ -54,5 +54,5 @@ export function listOmpSessionsFrom(path: string, cwd: string): OmpSessionEntry[
|
|
|
54
54
|
export function resolveListOmpSessions({ cwd }: RpcInput<typeof listOmpSessions>): {
|
|
55
55
|
sessions: OmpSessionEntry[];
|
|
56
56
|
} {
|
|
57
|
-
return { sessions: listOmpSessionsFrom(join(
|
|
57
|
+
return { sessions: listOmpSessionsFrom(join(ompDataDir(), "history.db"), cwd) };
|
|
58
58
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineSettings } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_COMPOSER_PILL_SETTINGS = {
|
|
5
|
+
mcp: true,
|
|
6
|
+
hub: true,
|
|
7
|
+
memory: true,
|
|
8
|
+
sessions: true,
|
|
9
|
+
quota: true,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const composerPillSettingsSchema = z.object({
|
|
13
|
+
mcp: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.mcp),
|
|
14
|
+
hub: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.hub),
|
|
15
|
+
memory: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.memory),
|
|
16
|
+
sessions: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.sessions),
|
|
17
|
+
quota: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.quota),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type ComposerPillSettings = z.infer<typeof composerPillSettingsSchema>;
|
|
21
|
+
export type ComposerPillKey = keyof ComposerPillSettings;
|
|
22
|
+
|
|
23
|
+
export const composerPillSettings = defineSettings({
|
|
24
|
+
id: "composer-pills",
|
|
25
|
+
scope: "host",
|
|
26
|
+
version: 1,
|
|
27
|
+
schema: composerPillSettingsSchema,
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ExternalUrlOpener = (url: string) => Promise<void>;
|
|
2
|
+
|
|
3
|
+
export function selectExternalUrlOpener(
|
|
4
|
+
paseoOpener: ExternalUrlOpener | undefined,
|
|
5
|
+
fallback: ExternalUrlOpener,
|
|
6
|
+
): ExternalUrlOpener {
|
|
7
|
+
return paseoOpener ?? fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validatedHttpUrl(value: string): string {
|
|
11
|
+
let url: URL;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(value);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error("Only absolute HTTP(S) URLs are supported.");
|
|
16
|
+
}
|
|
17
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
18
|
+
throw new Error("Only absolute HTTP(S) URLs are supported.");
|
|
19
|
+
}
|
|
20
|
+
return url.href;
|
|
21
|
+
}
|