@omercnet/paseo-omp 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 +87 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/SUPPORT.md +40 -0
- package/TESTING.md +147 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/memory-panel.tsx +71 -0
- package/client/memory-popover.tsx +70 -0
- package/client/omp-config-surface.tsx +1274 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +833 -0
- package/client/provider-diagnostics-state.ts +250 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +150 -0
- package/client/quota-state.ts +131 -0
- package/client/sessions-popover.tsx +73 -0
- package/docs/alpha-release-checklist.md +70 -0
- package/docs/configuration.md +122 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/installation.md +73 -0
- package/index.client.tsx +272 -0
- package/index.server.ts +51 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/server/hub.ts +145 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +126 -0
- package/server/omp-plugins.ts +627 -0
- package/server/omp-settings.ts +291 -0
- package/server/paths.ts +64 -0
- package/server/provider/catalog.ts +173 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +992 -0
- package/server/provider/host-tools.ts +706 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2739 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +151 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +431 -0
- package/server/provider/session.ts +4451 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +847 -0
- package/server/provider/timeline-projector.ts +1764 -0
- package/server/provider-diagnostics.ts +1057 -0
- package/server/quota.ts +54 -0
- package/server/sessions.ts +58 -0
- package/shared/hub.ts +43 -0
- package/shared/memory.ts +23 -0
- package/shared/omp-config.ts +81 -0
- package/shared/omp-plugins.ts +223 -0
- package/shared/omp-settings.ts +207 -0
- package/shared/provider-diagnostics.ts +117 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +22 -0
- package/shared/sessions.ts +23 -0
- package/tsconfig.json +16 -0
package/index.client.tsx
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { PaseoAgentListResult, PaseoApi } from "@getpaseo/client";
|
|
2
|
+
import type { PluginButtonRegistration, PluginClientContext } from "@getpaseo/plugin/client";
|
|
3
|
+
import { OmpIcon } from "./client/hub-icon";
|
|
4
|
+
import { HubPopover } from "./client/hub-popover";
|
|
5
|
+
import { summarizeHubProcesses } from "./client/hub-status";
|
|
6
|
+
import { OmpMemoryPanel } from "./client/memory-panel";
|
|
7
|
+
import { MemoryPopover } from "./client/memory-popover";
|
|
8
|
+
import { OmpConfigSurface } from "./client/omp-config-surface";
|
|
9
|
+
import { quotaProviderIcon } from "./client/provider-icon";
|
|
10
|
+
import { OmpImageTimeline } from "./client/provider-image";
|
|
11
|
+
import { QuotaPopover } from "./client/quota-popover";
|
|
12
|
+
import {
|
|
13
|
+
type QuotaSeverity,
|
|
14
|
+
quotaProviderFromSession,
|
|
15
|
+
quotaSeverityForProvider,
|
|
16
|
+
quotaSummaryForProvider,
|
|
17
|
+
} from "./client/quota-state";
|
|
18
|
+
import { SessionsPopover } from "./client/sessions-popover";
|
|
19
|
+
import { listHubProcesses } from "./shared/hub";
|
|
20
|
+
import { ompImageTimelineSchema, transformOmpImageToolItem } from "./shared/provider-image";
|
|
21
|
+
import { listOmpQuotas } from "./shared/quota";
|
|
22
|
+
|
|
23
|
+
const PAGE_LIMIT = 200;
|
|
24
|
+
const MAX_PAGES = 10;
|
|
25
|
+
const STATUS_POLL_MS = 4_000;
|
|
26
|
+
const QUOTA_POLL_MS = 30_000;
|
|
27
|
+
const RECONCILE_DEBOUNCE_MS = 250;
|
|
28
|
+
|
|
29
|
+
type AgentEntry = PaseoAgentListResult["entries"][number];
|
|
30
|
+
|
|
31
|
+
type PillEntry = {
|
|
32
|
+
cwd: string;
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
quotaProvider: string | null;
|
|
35
|
+
quotaSeverity: QuotaSeverity;
|
|
36
|
+
hub: PluginButtonRegistration;
|
|
37
|
+
memory: PluginButtonRegistration;
|
|
38
|
+
sessions: PluginButtonRegistration;
|
|
39
|
+
quota: PluginButtonRegistration;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
async function loadAgents(paseo: PaseoApi): Promise<AgentEntry[]> {
|
|
43
|
+
const entries: AgentEntry[] = [];
|
|
44
|
+
let cursor: string | undefined;
|
|
45
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
46
|
+
const result = await paseo.agents.list({
|
|
47
|
+
sort: [{ key: "updated_at", direction: "desc" }],
|
|
48
|
+
page: { limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
|
|
49
|
+
});
|
|
50
|
+
entries.push(...result.entries);
|
|
51
|
+
cursor = result.pageInfo.hasMore ? (result.pageInfo.nextCursor ?? undefined) : undefined;
|
|
52
|
+
if (!cursor) break;
|
|
53
|
+
}
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function contribute(client: PluginClientContext) {
|
|
58
|
+
const removeMemoryPanel = client.addWorkspacePanel({
|
|
59
|
+
id: "memory",
|
|
60
|
+
title: "OMP Memory",
|
|
61
|
+
icon: "Brain",
|
|
62
|
+
context: "workspace",
|
|
63
|
+
locations: ["explorer"],
|
|
64
|
+
Component: OmpMemoryPanel,
|
|
65
|
+
});
|
|
66
|
+
const removeOpenMemory = client.addCommandCenterItem({
|
|
67
|
+
id: "open-memory",
|
|
68
|
+
title: "Open OMP Memory",
|
|
69
|
+
icon: "Brain",
|
|
70
|
+
keywords: ["omp", "memory", "facts", "recall"],
|
|
71
|
+
context: "workspace",
|
|
72
|
+
onSelect({ openPanel }) {
|
|
73
|
+
openPanel("memory", { location: "explorer" });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const removeConfigSurface = client.addSurface("config", OmpConfigSurface);
|
|
77
|
+
const removeConfigSidebarItem = client.addSidebarItem({
|
|
78
|
+
id: "config",
|
|
79
|
+
title: "OMP",
|
|
80
|
+
icon: "Settings",
|
|
81
|
+
surface: "config",
|
|
82
|
+
});
|
|
83
|
+
const removeOpenConfig = client.addCommandCenterItem({
|
|
84
|
+
id: "open-config",
|
|
85
|
+
title: "Open OMP",
|
|
86
|
+
icon: "Settings",
|
|
87
|
+
keywords: ["omp", "config", "settings", "models", "providers"],
|
|
88
|
+
context: "global",
|
|
89
|
+
onSelect({ openSurface }) {
|
|
90
|
+
openSurface("config");
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const removeImageRenderer = client.addTimelineRenderer({
|
|
94
|
+
kind: "omp-images",
|
|
95
|
+
version: 1,
|
|
96
|
+
schema: ompImageTimelineSchema,
|
|
97
|
+
Component: OmpImageTimeline,
|
|
98
|
+
});
|
|
99
|
+
const removeImageTransformer = client.addTimelineTransformer({
|
|
100
|
+
id: "omp-images",
|
|
101
|
+
query: { itemType: "tool_call" },
|
|
102
|
+
transform({ item }) {
|
|
103
|
+
return transformOmpImageToolItem(item);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const pills = new Map<string, PillEntry>();
|
|
107
|
+
let disposed = false;
|
|
108
|
+
let reconcileTimer: ReturnType<typeof setTimeout> | undefined;
|
|
109
|
+
|
|
110
|
+
async function reconcile() {
|
|
111
|
+
const agents = await loadAgents(client.paseo);
|
|
112
|
+
if (disposed) return;
|
|
113
|
+
const activeIds = new Set<string>();
|
|
114
|
+
for (const { agent } of agents) {
|
|
115
|
+
if (agent.archivedAt || !agent.workspaceId || !agent.cwd) continue;
|
|
116
|
+
activeIds.add(agent.id);
|
|
117
|
+
const quotaProvider = quotaProviderFromSession(agent.provider, agent.model);
|
|
118
|
+
const current = pills.get(agent.id);
|
|
119
|
+
if (
|
|
120
|
+
current?.cwd === agent.cwd &&
|
|
121
|
+
current.workspaceId === agent.workspaceId &&
|
|
122
|
+
current.quotaProvider === quotaProvider
|
|
123
|
+
) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
current?.hub.remove();
|
|
127
|
+
current?.memory.remove();
|
|
128
|
+
current?.sessions.remove();
|
|
129
|
+
current?.quota.remove();
|
|
130
|
+
pills.set(agent.id, {
|
|
131
|
+
cwd: agent.cwd,
|
|
132
|
+
workspaceId: agent.workspaceId,
|
|
133
|
+
quotaProvider,
|
|
134
|
+
quotaSeverity: "unknown",
|
|
135
|
+
hub: client.addComposerPill({
|
|
136
|
+
id: "hub",
|
|
137
|
+
workspaceId: agent.workspaceId,
|
|
138
|
+
agentId: agent.id,
|
|
139
|
+
button: {
|
|
140
|
+
title: "Hub processes",
|
|
141
|
+
icon: OmpIcon,
|
|
142
|
+
label: "Hub",
|
|
143
|
+
visible: false,
|
|
144
|
+
behavior: { kind: "popover", Content: HubPopover },
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
memory: client.addComposerPill({
|
|
148
|
+
id: "memory",
|
|
149
|
+
workspaceId: agent.workspaceId,
|
|
150
|
+
agentId: agent.id,
|
|
151
|
+
button: {
|
|
152
|
+
title: "OMP workspace memory",
|
|
153
|
+
icon: "Brain",
|
|
154
|
+
label: "Memory",
|
|
155
|
+
behavior: { kind: "popover", Content: MemoryPopover },
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
sessions: client.addComposerPill({
|
|
159
|
+
id: "sessions",
|
|
160
|
+
workspaceId: agent.workspaceId,
|
|
161
|
+
agentId: agent.id,
|
|
162
|
+
button: {
|
|
163
|
+
title: "omp session history",
|
|
164
|
+
icon: "History",
|
|
165
|
+
label: "Sessions",
|
|
166
|
+
behavior: { kind: "popover", Content: SessionsPopover },
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
quota: client.addComposerPill({
|
|
170
|
+
id: "quota",
|
|
171
|
+
workspaceId: agent.workspaceId,
|
|
172
|
+
agentId: agent.id,
|
|
173
|
+
button: {
|
|
174
|
+
title: "OMP provider quotas",
|
|
175
|
+
icon: quotaProviderIcon(quotaProvider, "unknown"),
|
|
176
|
+
label: "Quota",
|
|
177
|
+
visible: false,
|
|
178
|
+
behavior: { kind: "popover", Content: QuotaPopover },
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (const [agentId, pill] of pills) {
|
|
184
|
+
if (activeIds.has(agentId)) continue;
|
|
185
|
+
pill.hub.remove();
|
|
186
|
+
pill.memory.remove();
|
|
187
|
+
pill.sessions.remove();
|
|
188
|
+
pill.quota.remove();
|
|
189
|
+
pills.delete(agentId);
|
|
190
|
+
}
|
|
191
|
+
await Promise.all([refreshHubStatus(), refreshQuotaStatus()]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function refreshHubStatus() {
|
|
195
|
+
const pillsByCwd = new Map<string, PillEntry[]>();
|
|
196
|
+
for (const pill of pills.values()) {
|
|
197
|
+
const group = pillsByCwd.get(pill.cwd);
|
|
198
|
+
if (group) group.push(pill);
|
|
199
|
+
else pillsByCwd.set(pill.cwd, [pill]);
|
|
200
|
+
}
|
|
201
|
+
await Promise.all(
|
|
202
|
+
[...pillsByCwd].map(async ([cwd, cwdPills]) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await client.rpc(listHubProcesses, { cwd });
|
|
205
|
+
if (disposed) return;
|
|
206
|
+
const summary = summarizeHubProcesses(result.processes);
|
|
207
|
+
for (const pill of cwdPills) pill.hub.update(summary);
|
|
208
|
+
} catch {
|
|
209
|
+
// Keep the last known state. A disconnected host or missing omp directory should not
|
|
210
|
+
// remove a status the user was already inspecting.
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function refreshQuotaStatus() {
|
|
217
|
+
try {
|
|
218
|
+
const result = await client.rpc(listOmpQuotas, {});
|
|
219
|
+
if (disposed) return;
|
|
220
|
+
for (const pill of pills.values()) {
|
|
221
|
+
const severity = quotaSeverityForProvider(result.quotas, pill.quotaProvider);
|
|
222
|
+
pill.quota.update({
|
|
223
|
+
...quotaSummaryForProvider(result.quotas, pill.quotaProvider),
|
|
224
|
+
...(severity === pill.quotaSeverity
|
|
225
|
+
? {}
|
|
226
|
+
: { icon: quotaProviderIcon(pill.quotaProvider, severity) }),
|
|
227
|
+
});
|
|
228
|
+
pill.quotaSeverity = severity;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// The quota database is optional and may not exist on a new omp installation.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function scheduleReconcile() {
|
|
236
|
+
clearTimeout(reconcileTimer);
|
|
237
|
+
reconcileTimer = setTimeout(() => {
|
|
238
|
+
void reconcile().catch(() => {});
|
|
239
|
+
}, RECONCILE_DEBOUNCE_MS);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const unsubscribeAgents = client.paseo.agents.subscribe(scheduleReconcile);
|
|
243
|
+
const hubPoll = setInterval(() => {
|
|
244
|
+
void refreshHubStatus().catch(() => {});
|
|
245
|
+
}, STATUS_POLL_MS);
|
|
246
|
+
const quotaPoll = setInterval(() => {
|
|
247
|
+
void refreshQuotaStatus().catch(() => {});
|
|
248
|
+
}, QUOTA_POLL_MS);
|
|
249
|
+
void reconcile().catch(() => {});
|
|
250
|
+
|
|
251
|
+
return () => {
|
|
252
|
+
disposed = true;
|
|
253
|
+
unsubscribeAgents();
|
|
254
|
+
clearTimeout(reconcileTimer);
|
|
255
|
+
clearInterval(hubPoll);
|
|
256
|
+
clearInterval(quotaPoll);
|
|
257
|
+
for (const pill of pills.values()) {
|
|
258
|
+
pill.hub.remove();
|
|
259
|
+
pill.memory.remove();
|
|
260
|
+
pill.sessions.remove();
|
|
261
|
+
pill.quota.remove();
|
|
262
|
+
}
|
|
263
|
+
pills.clear();
|
|
264
|
+
removeImageRenderer();
|
|
265
|
+
removeImageTransformer();
|
|
266
|
+
removeOpenConfig();
|
|
267
|
+
removeConfigSidebarItem();
|
|
268
|
+
removeConfigSurface();
|
|
269
|
+
removeOpenMemory();
|
|
270
|
+
removeMemoryPanel();
|
|
271
|
+
};
|
|
272
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { resolveListHubProcesses, resolveTailHubLog } from "./server/hub";
|
|
3
|
+
import { resolveListOmpMemory } from "./server/memory";
|
|
4
|
+
import { resolveListOmpConfig } from "./server/omp-config";
|
|
5
|
+
import {
|
|
6
|
+
resolveInspectOmpPluginConfig,
|
|
7
|
+
resolveListOmpPlugins,
|
|
8
|
+
resolveMutateOmpPlugin,
|
|
9
|
+
resolveMutateOmpPluginConfig,
|
|
10
|
+
} from "./server/omp-plugins";
|
|
11
|
+
import { resolveListOmpSettings, resolveUpdateOmpSettings } from "./server/omp-settings";
|
|
12
|
+
import { withOmpWorkspaceIdentity } from "./server/provider/host-tools";
|
|
13
|
+
import { createOmpProvider } from "./server/provider/registration";
|
|
14
|
+
import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
|
|
15
|
+
import { resolveListOmpQuotas } from "./server/quota";
|
|
16
|
+
import { resolveListOmpSessions } from "./server/sessions";
|
|
17
|
+
import { listHubProcesses, tailHubLog } from "./shared/hub";
|
|
18
|
+
import { listOmpMemory } from "./shared/memory";
|
|
19
|
+
import { listOmpConfig } from "./shared/omp-config";
|
|
20
|
+
import {
|
|
21
|
+
inspectOmpPluginConfig,
|
|
22
|
+
listOmpPlugins,
|
|
23
|
+
mutateOmpPlugin,
|
|
24
|
+
mutateOmpPluginConfig,
|
|
25
|
+
} from "./shared/omp-plugins";
|
|
26
|
+
import { listOmpSettings, updateOmpSettings } from "./shared/omp-settings";
|
|
27
|
+
import { getOmpProviderHealth } from "./shared/provider-diagnostics";
|
|
28
|
+
import { listOmpQuotas } from "./shared/quota";
|
|
29
|
+
import { listOmpSessions } from "./shared/sessions";
|
|
30
|
+
|
|
31
|
+
export default function contribute(server: PluginServerContext) {
|
|
32
|
+
server.handle(listHubProcesses, resolveListHubProcesses);
|
|
33
|
+
server.handle(tailHubLog, resolveTailHubLog);
|
|
34
|
+
server.handle(listOmpQuotas, resolveListOmpQuotas);
|
|
35
|
+
server.handle(listOmpMemory, resolveListOmpMemory);
|
|
36
|
+
server.handle(listOmpSessions, resolveListOmpSessions);
|
|
37
|
+
server.handle(listOmpConfig, resolveListOmpConfig);
|
|
38
|
+
server.handle(listOmpPlugins, resolveListOmpPlugins);
|
|
39
|
+
server.handle(inspectOmpPluginConfig, resolveInspectOmpPluginConfig);
|
|
40
|
+
server.handle(mutateOmpPlugin, resolveMutateOmpPlugin);
|
|
41
|
+
server.handle(mutateOmpPluginConfig, resolveMutateOmpPluginConfig);
|
|
42
|
+
server.handle(listOmpSettings, resolveListOmpSettings);
|
|
43
|
+
server.handle(updateOmpSettings, resolveUpdateOmpSettings);
|
|
44
|
+
server.handle(getOmpProviderHealth, resolveGetOmpProviderHealth);
|
|
45
|
+
const removeIdentityHook = server.before("agent.session_open", ({ request }) => {
|
|
46
|
+
if (request.provider !== "omp-plugin") return;
|
|
47
|
+
return withOmpWorkspaceIdentity(request);
|
|
48
|
+
});
|
|
49
|
+
server.registerProvider(createOmpProvider());
|
|
50
|
+
return () => removeIdentityHook();
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omercnet/paseo-omp",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/omercnet/paseo-plugins.git",
|
|
10
|
+
"directory": "paseo-omp"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/omercnet/paseo-plugins/tree/main/paseo-omp#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/omercnet/paseo-plugins/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"packageManager": "npm@10.9.4",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.18.0"
|
|
22
|
+
},
|
|
23
|
+
"author": "Omer Cohen",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"paseo",
|
|
26
|
+
"paseo-plugin",
|
|
27
|
+
"paseo-omp",
|
|
28
|
+
"omp",
|
|
29
|
+
"coding-agents"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"package-lock.json",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md",
|
|
36
|
+
"SUPPORT.md",
|
|
37
|
+
"TESTING.md",
|
|
38
|
+
"docs",
|
|
39
|
+
"index.client.tsx",
|
|
40
|
+
"index.server.ts",
|
|
41
|
+
"client",
|
|
42
|
+
"server",
|
|
43
|
+
"shared",
|
|
44
|
+
"paseo-plugin.json",
|
|
45
|
+
"tsconfig.json"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check": "biome check .",
|
|
49
|
+
"check:write": "biome check --write .",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:coverage": "PASEO_OMP_COVERAGE=1 vitest run --coverage",
|
|
52
|
+
"test:integration:docker": "node --import tsx scripts/test-docker-host-tools.ts",
|
|
53
|
+
"test:integration:wsl": "node --import tsx scripts/test-wsl-host-tools.ts",
|
|
54
|
+
"test:integration:install": "node --import tsx scripts/test-package-install.ts",
|
|
55
|
+
"package:release": "node --import tsx scripts/package-release.ts",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
60
|
+
"yaml": "^2.9.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@biomejs/biome": "^2.5.10",
|
|
64
|
+
"@getpaseo/cli": "0.8.0",
|
|
65
|
+
"@getpaseo/client": "0.8.0",
|
|
66
|
+
"@getpaseo/plugin": "0.8.0",
|
|
67
|
+
"@getpaseo/protocol": "0.8.0",
|
|
68
|
+
"@tanstack/react-query": "^5.102.3",
|
|
69
|
+
"@types/node": "^24.10.1",
|
|
70
|
+
"@types/react": "^19.2.18",
|
|
71
|
+
"esbuild": "^0.28.0",
|
|
72
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
73
|
+
"fflate": "^0.8.3",
|
|
74
|
+
"react": "19.1.0",
|
|
75
|
+
"react-native": "0.81.5",
|
|
76
|
+
"tsx": "^4.20.6",
|
|
77
|
+
"typescript": "^7.0.2",
|
|
78
|
+
"vitest": "^5.0.0",
|
|
79
|
+
"zod": "^4.4.3"
|
|
80
|
+
},
|
|
81
|
+
"overrides": {
|
|
82
|
+
"qs": "6.16.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/server/hub.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { HubProcess, listHubProcesses, tailHubLog } from "../shared/hub";
|
|
7
|
+
|
|
8
|
+
const MAX_LOG_BYTES = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
const ScopeFileSchema = z.object({ projectDir: z.string() });
|
|
11
|
+
const MetaFileSchema = z.object({
|
|
12
|
+
daemon: z.object({
|
|
13
|
+
state: z.string(),
|
|
14
|
+
owner: z.string().optional(),
|
|
15
|
+
restartCount: z.number().optional(),
|
|
16
|
+
persist: z.boolean().optional(),
|
|
17
|
+
detached: z.boolean().optional(),
|
|
18
|
+
createdAt: z.number().optional(),
|
|
19
|
+
startedAt: z.number().optional(),
|
|
20
|
+
readyAt: z.number().optional(),
|
|
21
|
+
exitedAt: z.number().optional(),
|
|
22
|
+
exitCode: z.number().optional(),
|
|
23
|
+
}),
|
|
24
|
+
spec: z.object({
|
|
25
|
+
application: z.string(),
|
|
26
|
+
args: z.array(z.string()).optional(),
|
|
27
|
+
cwd: z.string().optional(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* omp's hub keeps per-project process state at
|
|
33
|
+
* ~/.omp/run/daemons/<projectHash>/{scope.json, daemons/<name>/{meta.json,output.log}}.
|
|
34
|
+
* `scope.json.projectDir` matches a workspace's cwd exactly, so a project's hub processes can
|
|
35
|
+
* be resolved without any cooperation from the omp process itself.
|
|
36
|
+
*
|
|
37
|
+
* This is an internal, unversioned implementation detail of the omp harness: every read below
|
|
38
|
+
* is best-effort and degrades to an empty/partial result instead of throwing when a file is
|
|
39
|
+
* missing, unreadable, or shaped differently than expected (a future omp release is free to
|
|
40
|
+
* change or remove this layout).
|
|
41
|
+
*/
|
|
42
|
+
function ompRunDir(): string {
|
|
43
|
+
return process.env.PASEO_OMP_RUN_DIR ?? join(homedir(), ".omp", "run", "daemons");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readJsonFile(path: string): Promise<unknown | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function listDirNames(path: string): Promise<string[]> {
|
|
55
|
+
try {
|
|
56
|
+
return await readdir(path);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function findProjectDaemonRoots(root: string, cwd: string): Promise<string[]> {
|
|
63
|
+
const hashes = await listDirNames(root);
|
|
64
|
+
const matches: string[] = [];
|
|
65
|
+
await Promise.all(
|
|
66
|
+
hashes.map(async (hash) => {
|
|
67
|
+
const scope = ScopeFileSchema.safeParse(await readJsonFile(join(root, hash, "scope.json")));
|
|
68
|
+
if (scope.success && scope.data.projectDir === cwd) matches.push(join(root, hash));
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return matches;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toHubProcess(name: string, value: unknown): HubProcess | undefined {
|
|
75
|
+
const parsed = MetaFileSchema.safeParse(value);
|
|
76
|
+
if (!parsed.success) return undefined;
|
|
77
|
+
const { daemon, spec } = parsed.data;
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
application: spec.application,
|
|
81
|
+
args: spec.args ?? [],
|
|
82
|
+
cwd: spec.cwd ?? "",
|
|
83
|
+
state: daemon.state,
|
|
84
|
+
owner: daemon.owner ?? null,
|
|
85
|
+
restartCount: daemon.restartCount ?? 0,
|
|
86
|
+
persist: daemon.persist ?? false,
|
|
87
|
+
detached: daemon.detached ?? false,
|
|
88
|
+
createdAt: daemon.createdAt ?? null,
|
|
89
|
+
startedAt: daemon.startedAt ?? null,
|
|
90
|
+
readyAt: daemon.readyAt ?? null,
|
|
91
|
+
exitedAt: daemon.exitedAt ?? null,
|
|
92
|
+
exitCode: daemon.exitCode ?? null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function listHubProcessesFrom(root: string, cwd: string): Promise<HubProcess[]> {
|
|
97
|
+
const roots = await findProjectDaemonRoots(root, cwd);
|
|
98
|
+
const processes: HubProcess[] = [];
|
|
99
|
+
await Promise.all(
|
|
100
|
+
roots.map(async (projectRoot) => {
|
|
101
|
+
const names = await listDirNames(join(projectRoot, "daemons"));
|
|
102
|
+
await Promise.all(
|
|
103
|
+
names.map(async (name) => {
|
|
104
|
+
const meta = await readJsonFile(join(projectRoot, "daemons", name, "meta.json"));
|
|
105
|
+
const process = toHubProcess(name, meta);
|
|
106
|
+
if (process) processes.push(process);
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
processes.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
112
|
+
return processes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function resolveListHubProcesses({
|
|
116
|
+
cwd,
|
|
117
|
+
}: RpcInput<typeof listHubProcesses>): Promise<{ processes: HubProcess[] }> {
|
|
118
|
+
return { processes: await listHubProcessesFrom(ompRunDir(), cwd) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function tailHubLogFrom(
|
|
122
|
+
root: string,
|
|
123
|
+
cwd: string,
|
|
124
|
+
name: string,
|
|
125
|
+
): Promise<{ content: string; truncated: boolean }> {
|
|
126
|
+
const roots = await findProjectDaemonRoots(root, cwd);
|
|
127
|
+
for (const projectRoot of roots) {
|
|
128
|
+
try {
|
|
129
|
+
const buffer = await readFile(join(projectRoot, "daemons", name, "output.log"));
|
|
130
|
+
const truncated = buffer.byteLength > MAX_LOG_BYTES;
|
|
131
|
+
const slice = truncated ? buffer.subarray(buffer.byteLength - MAX_LOG_BYTES) : buffer;
|
|
132
|
+
return { content: slice.toString("utf8"), truncated };
|
|
133
|
+
} catch {
|
|
134
|
+
// This root doesn't have that process (or its log vanished); try the next match.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { content: "", truncated: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function resolveTailHubLog({
|
|
141
|
+
cwd,
|
|
142
|
+
name,
|
|
143
|
+
}: RpcInput<typeof tailHubLog>): Promise<{ content: string; truncated: boolean }> {
|
|
144
|
+
return tailHubLogFrom(ompRunDir(), cwd, name);
|
|
145
|
+
}
|
package/server/memory.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { listOmpMemory, OmpMemoryFact } from "../shared/memory";
|
|
7
|
+
import { ompAgentDir } from "./paths";
|
|
8
|
+
|
|
9
|
+
const FactRowSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
subject: z.string(),
|
|
12
|
+
predicate: z.string(),
|
|
13
|
+
object: z.string(),
|
|
14
|
+
confidence: z.number().min(0).max(1).nullable(),
|
|
15
|
+
timestamp: z.string().nullable(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
type CandidateBank = { name: string; modifiedAt: number };
|
|
19
|
+
|
|
20
|
+
async function newestBank(root: string, cwd: string): Promise<string | undefined> {
|
|
21
|
+
const prefix = `${basename(cwd)}-`;
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
24
|
+
const candidates = await Promise.all(
|
|
25
|
+
entries
|
|
26
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
27
|
+
.map(async (entry): Promise<CandidateBank | undefined> => {
|
|
28
|
+
try {
|
|
29
|
+
return { name: entry.name, modifiedAt: (await stat(join(root, entry.name))).mtimeMs };
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return candidates
|
|
36
|
+
.flatMap((candidate) => (candidate ? [candidate] : []))
|
|
37
|
+
.sort((a, b) => b.modifiedAt - a.modifiedAt)[0]?.name;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function listOmpFactsFrom(path: string): OmpMemoryFact[] {
|
|
44
|
+
try {
|
|
45
|
+
const database = new DatabaseSync(path, { readOnly: true, timeout: 500 });
|
|
46
|
+
try {
|
|
47
|
+
const rows = database
|
|
48
|
+
.prepare(
|
|
49
|
+
`SELECT fact_id AS id, subject, predicate, object, confidence, timestamp
|
|
50
|
+
FROM facts
|
|
51
|
+
ORDER BY COALESCE(timestamp, created_at) DESC
|
|
52
|
+
LIMIT 100`,
|
|
53
|
+
)
|
|
54
|
+
.all();
|
|
55
|
+
return rows.flatMap((row) => {
|
|
56
|
+
const parsed = FactRowSchema.safeParse(row);
|
|
57
|
+
if (!parsed.success) return [];
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
...parsed.data,
|
|
61
|
+
confidence: parsed.data.confidence ?? 1,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
database.close();
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function listOmpMemoryFrom(
|
|
74
|
+
root: string,
|
|
75
|
+
cwd: string,
|
|
76
|
+
): Promise<{ bank: string | null; facts: OmpMemoryFact[] }> {
|
|
77
|
+
const bank = await newestBank(root, cwd);
|
|
78
|
+
if (!bank) return { bank: null, facts: [] };
|
|
79
|
+
return { bank, facts: listOmpFactsFrom(join(root, bank, "mnemopi.db")) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function resolveListOmpMemory({
|
|
83
|
+
cwd,
|
|
84
|
+
}: RpcInput<typeof listOmpMemory>): Promise<{ bank: string | null; facts: OmpMemoryFact[] }> {
|
|
85
|
+
return listOmpMemoryFrom(join(ompAgentDir(), "memories", "mnemopi", "banks"), cwd);
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class SerialMutationQueue {
|
|
2
|
+
private tail: Promise<void> = Promise.resolve();
|
|
3
|
+
|
|
4
|
+
run<T>(operation: () => Promise<T>): Promise<T> {
|
|
5
|
+
const result = this.tail.then(operation, operation);
|
|
6
|
+
this.tail = result.then(
|
|
7
|
+
() => undefined,
|
|
8
|
+
() => undefined,
|
|
9
|
+
);
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
}
|