@xpufx/paseo-top 0.4.2 → 0.4.4
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/README.md +11 -12
- package/client/multi-host-view.tsx +177 -0
- package/client/multi-host.ts +98 -0
- package/client/pill-labels.ts +30 -6
- package/client/pill.tsx +42 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +10 -9
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
- package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
- package/client/vendor/paseo-plugin-helper/host.ts +12 -58
- package/client/vendor/paseo-plugin-helper/index.ts +2 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
- package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
- package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
- package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +12 -3
- package/paseo-plugin.json +1 -1
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +348 -0
- package/shared/resources.ts +1 -1
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
- package/shared/version.ts +1 -1
package/index.server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { guardRpcHandler } from "./server/vendor/paseo-plugin-helper/index";
|
|
3
|
+
import { registerPermissionAuditServer } from "permission-audit/server";
|
|
3
4
|
import {
|
|
4
5
|
getSystemResourcesRpc,
|
|
5
6
|
getCustomPillsRpc,
|
|
@@ -74,6 +75,11 @@ export default function contribute(server: PluginServerContext) {
|
|
|
74
75
|
server.handle(listCustomPillsRpc, handleListCustomPills);
|
|
75
76
|
server.handle(runCustomPillModalCommandRpc, handleRunCustomPillModalCommand);
|
|
76
77
|
|
|
78
|
+
const { unsubscribe: unsubscribePermissionAudit } = registerPermissionAuditServer(
|
|
79
|
+
server as unknown as Parameters<typeof registerPermissionAuditServer>[0],
|
|
80
|
+
{ logger: log },
|
|
81
|
+
);
|
|
82
|
+
|
|
77
83
|
const turnStartTimes = new Map<string, number>();
|
|
78
84
|
const turnGitBefore = new Map<string, { insertions: number; deletions: number; filesChanged: number }>();
|
|
79
85
|
// Live turn per agent, set on turn_started and consumed by its terminal, used
|
|
@@ -283,6 +289,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
283
289
|
lastCanceledAt.clear();
|
|
284
290
|
lastCanceledUserMessages.clear();
|
|
285
291
|
customPillPoller.stop();
|
|
292
|
+
unsubscribePermissionAudit();
|
|
286
293
|
unsubscribeTurnStarted();
|
|
287
294
|
unsubscribeAgentCreated();
|
|
288
295
|
unsubscribeTurnEnded();
|
package/package.json
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-top",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.4",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"permission-audit": "*"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
|
|
8
11
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"test": "npm run typecheck &&
|
|
12
|
+
"test": "npm run typecheck && vitest run --config client/vitest.config.ts"
|
|
10
13
|
},
|
|
11
14
|
"devDependencies": {
|
|
12
|
-
"@getpaseo/plugin": "0.9.0
|
|
15
|
+
"@getpaseo/plugin": "^0.9.0",
|
|
13
16
|
"@tanstack/react-query": "^5.90.11",
|
|
14
17
|
"@types/react": "^19.0.10",
|
|
18
|
+
"@types/react-test-renderer": "^19.3.0",
|
|
15
19
|
"react": "19.1.0",
|
|
16
20
|
"react-native": "0.81.5",
|
|
21
|
+
"react-test-renderer": "^19.1.0",
|
|
17
22
|
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^3.0.0",
|
|
18
24
|
"zod": "^4.4.3"
|
|
19
25
|
},
|
|
20
26
|
"files": [
|
|
@@ -30,6 +36,9 @@
|
|
|
30
36
|
"!**/*.test.ts",
|
|
31
37
|
"!**/*.test.tsx",
|
|
32
38
|
"!**/*.test.mjs",
|
|
39
|
+
"!client/test-utils",
|
|
40
|
+
"!server/__node-test-shim.ts",
|
|
41
|
+
"!client/vitest.config.ts",
|
|
33
42
|
"!**/.DS_Store"
|
|
34
43
|
],
|
|
35
44
|
"repository": {
|
package/paseo-plugin.json
CHANGED
|
@@ -4,6 +4,7 @@ export interface McpStdioInjectionConfig {
|
|
|
4
4
|
args?: string[];
|
|
5
5
|
env?: Record<string, string>;
|
|
6
6
|
alwaysLoad?: boolean;
|
|
7
|
+
instructions?: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface McpHttpInjectionConfig {
|
|
@@ -11,6 +12,7 @@ export interface McpHttpInjectionConfig {
|
|
|
11
12
|
url: string;
|
|
12
13
|
headers?: Record<string, string>;
|
|
13
14
|
alwaysLoad?: boolean;
|
|
15
|
+
instructions?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface McpSseInjectionConfig {
|
|
@@ -18,6 +20,7 @@ export interface McpSseInjectionConfig {
|
|
|
18
20
|
url: string;
|
|
19
21
|
headers?: Record<string, string>;
|
|
20
22
|
alwaysLoad?: boolean;
|
|
23
|
+
instructions?: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export type McpInjectionConfig =
|
|
@@ -71,15 +74,30 @@ export function registerMcpInjection(
|
|
|
71
74
|
const { serverName, config, filter } = options;
|
|
72
75
|
return server.before("agent.create", ({ request }: { request: AgentCreateInjectionRequest }) => {
|
|
73
76
|
if (filter && !filter({ request })) return;
|
|
74
|
-
|
|
77
|
+
const injectedConfig = { ...config };
|
|
78
|
+
if (injectedConfig.instructions === "") {
|
|
79
|
+
delete injectedConfig.instructions;
|
|
80
|
+
}
|
|
81
|
+
const composed: AgentCreateInjectionRequest = {
|
|
75
82
|
...request,
|
|
76
83
|
config: {
|
|
77
84
|
...request.config,
|
|
78
85
|
mcpServers: {
|
|
79
86
|
...(request.config.mcpServers ?? {}),
|
|
80
|
-
[serverName]:
|
|
87
|
+
[serverName]: injectedConfig,
|
|
81
88
|
},
|
|
82
89
|
},
|
|
83
90
|
};
|
|
91
|
+
// Opt-in per server: only a config that sets `instructions` gets text
|
|
92
|
+
// composed into the agent. Absent means byte-identical passthrough.
|
|
93
|
+
if (typeof config.instructions !== "string" || config.instructions === "") return composed;
|
|
94
|
+
const base = typeof request.config.systemPrompt === "string" ? request.config.systemPrompt : "";
|
|
95
|
+
return {
|
|
96
|
+
...composed,
|
|
97
|
+
config: {
|
|
98
|
+
...composed.config,
|
|
99
|
+
systemPrompt: base.trim().length > 0 ? `${base}\n\n${config.instructions}` : config.instructions,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
84
102
|
});
|
|
85
103
|
}
|
|
@@ -26,15 +26,54 @@ export interface ListPluginsOptions {
|
|
|
26
26
|
forceRefresh?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Normalized presence record returned to plugin code by `listPlugins(context)`. */
|
|
30
|
+
export interface PluginPresence {
|
|
31
|
+
id: string;
|
|
32
|
+
status: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal structural view of the daemon plugin surface a plugin may reach
|
|
38
|
+
* through `context.paseo`. Current SDK `PaseoApi` exposes no plugins actions,
|
|
39
|
+
* so this stays optional and duck-typed; when absent the helper falls back to
|
|
40
|
+
* the daemon's existing `paseo plugin ls --json` query.
|
|
41
|
+
*/
|
|
42
|
+
export interface PaseoPluginsSurface {
|
|
43
|
+
list(): Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handler/hook context shape plugin server code passes to the presence API.
|
|
48
|
+
* `paseo` stays `unknown` so the real `PluginHandlerContext` (whose `paseo` is
|
|
49
|
+
* the SDK `PaseoApi`) is assignable even though current SDK builds expose no
|
|
50
|
+
* plugins actions; the surface is duck-typed at call time.
|
|
51
|
+
*/
|
|
52
|
+
export interface PluginRegistryContext {
|
|
53
|
+
paseo?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PluginQueryOptions {
|
|
57
|
+
cacheTtlMs?: number;
|
|
58
|
+
forceRefresh?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PresenceCache {
|
|
62
|
+
at: number;
|
|
63
|
+
plugins: PluginPresence[];
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
let cachedPlugins: PaseoPluginInfo[] | null = null;
|
|
30
67
|
let lastFetchTime = 0;
|
|
68
|
+
let cachedPresence: PresenceCache | null = null;
|
|
31
69
|
|
|
32
70
|
/**
|
|
33
|
-
* Clears the in-memory plugin list
|
|
71
|
+
* Clears the in-memory plugin list and presence caches.
|
|
34
72
|
*/
|
|
35
73
|
export function clearPluginCache(): void {
|
|
36
74
|
cachedPlugins = null;
|
|
37
75
|
lastFetchTime = 0;
|
|
76
|
+
cachedPresence = null;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
/**
|
|
@@ -68,10 +107,30 @@ function readConfigPluginsFallback(): PaseoPluginInfo[] {
|
|
|
68
107
|
}
|
|
69
108
|
|
|
70
109
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
110
|
+
* Queries the daemon's authoritative plugin list through the CLI RPC that the
|
|
111
|
+
* daemon already serves ('paseo plugin ls --json'). Returns null when the daemon
|
|
112
|
+
* is unreachable or answers with a non-array payload, so callers can choose
|
|
113
|
+
* their own fallback instead of parsing filesystem state.
|
|
114
|
+
*/
|
|
115
|
+
async function fetchDaemonPluginInfo(): Promise<PaseoPluginInfo[] | null> {
|
|
116
|
+
try {
|
|
117
|
+
const result = await safeSpawn("paseo", ["plugin", "ls", "--json"], { timeoutMs: 3000 });
|
|
118
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
119
|
+
const parsed: unknown = JSON.parse(result.stdout.trim());
|
|
120
|
+
return Array.isArray(parsed) ? (parsed as PaseoPluginInfo[]) : null;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Unreachable daemon / malformed payload: report absence, never throw.
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Lists full plugin metadata from the Paseo daemon, with optional filtering and
|
|
130
|
+
* TTL caching. Primary mechanism uses 'paseo plugin ls --json', with fallback to
|
|
131
|
+
* ~/.paseo/config.json.
|
|
73
132
|
*/
|
|
74
|
-
|
|
133
|
+
async function listPluginInfo(options: ListPluginsOptions = {}): Promise<PaseoPluginInfo[]> {
|
|
75
134
|
const { filter = "all", cacheTtlMs = 5000, forceRefresh = false } = options;
|
|
76
135
|
const now = Date.now();
|
|
77
136
|
|
|
@@ -79,23 +138,102 @@ export async function listPlugins(options: ListPluginsOptions = {}): Promise<Pas
|
|
|
79
138
|
return applyFilter(cachedPlugins, filter);
|
|
80
139
|
}
|
|
81
140
|
|
|
82
|
-
|
|
141
|
+
const fromDaemon = await fetchDaemonPluginInfo();
|
|
142
|
+
const plugins = fromDaemon ?? readConfigPluginsFallback();
|
|
83
143
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
144
|
+
cachedPlugins = plugins;
|
|
145
|
+
lastFetchTime = now;
|
|
146
|
+
|
|
147
|
+
return applyFilter(plugins, filter);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isRegistryContext(value: unknown): value is PluginRegistryContext {
|
|
151
|
+
return !!value && typeof value === "object" && "paseo" in (value as Record<string, unknown>);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Coerces a daemon plugin list (SDK shape) into normalized presence records. */
|
|
155
|
+
function normalizePresence(raw: unknown): PluginPresence[] {
|
|
156
|
+
if (!Array.isArray(raw)) return [];
|
|
157
|
+
const out: PluginPresence[] = [];
|
|
158
|
+
for (const entry of raw) {
|
|
159
|
+
if (!entry || typeof entry !== "object") continue;
|
|
160
|
+
const rec = entry as Record<string, unknown>;
|
|
161
|
+
const id = typeof rec.id === "string" ? rec.id : null;
|
|
162
|
+
if (!id) continue;
|
|
163
|
+
const enabled = rec.enabled !== false;
|
|
164
|
+
const status =
|
|
165
|
+
typeof rec.status === "string" ? rec.status : enabled ? "unknown" : "disabled";
|
|
166
|
+
out.push({ id, status, enabled });
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolves plugin presence through the sanctioned daemon channel. Prefers a
|
|
173
|
+
* `context.paseo.plugins.list()` SDK surface when one exists; otherwise falls
|
|
174
|
+
* back to the daemon's existing plugin-list RPC. Any failure — no context, no
|
|
175
|
+
* surface, unreachable daemon, malformed payload — resolves to `[]`.
|
|
176
|
+
*
|
|
177
|
+
* Only the daemon-fallback result is cached (the CLI spawn is the real cost);
|
|
178
|
+
* a caller-supplied context surface is always consulted fresh so different
|
|
179
|
+
* contexts never observe each other's cached presence.
|
|
180
|
+
*/
|
|
181
|
+
async function listPluginPresence(
|
|
182
|
+
context: PluginRegistryContext | null | undefined,
|
|
183
|
+
options: PluginQueryOptions = {},
|
|
184
|
+
): Promise<PluginPresence[]> {
|
|
185
|
+
const { cacheTtlMs = 5000, forceRefresh = false } = options;
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const surface = readPluginsSurface(context?.paseo);
|
|
188
|
+
|
|
189
|
+
if (!surface) {
|
|
190
|
+
if (!forceRefresh && cachedPresence && now - cachedPresence.at < cacheTtlMs) {
|
|
191
|
+
return cachedPresence.plugins;
|
|
90
192
|
}
|
|
193
|
+
const plugins = normalizePresence(await fetchDaemonPluginInfo());
|
|
194
|
+
cachedPresence = { at: now, plugins };
|
|
195
|
+
return plugins;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
return normalizePresence(await surface.list());
|
|
91
200
|
} catch {
|
|
92
|
-
|
|
201
|
+
return [];
|
|
93
202
|
}
|
|
203
|
+
}
|
|
94
204
|
|
|
95
|
-
|
|
96
|
-
|
|
205
|
+
/** Duck-types `context.paseo.plugins.list()` without assuming the SDK shape. */
|
|
206
|
+
function readPluginsSurface(paseo: unknown): PaseoPluginsSurface | null {
|
|
207
|
+
if (!paseo || typeof paseo !== "object") return null;
|
|
208
|
+
const plugins = (paseo as { plugins?: unknown }).plugins;
|
|
209
|
+
if (!plugins || typeof plugins !== "object") return null;
|
|
210
|
+
const list = (plugins as { list?: unknown }).list;
|
|
211
|
+
return typeof list === "function" ? (plugins as PaseoPluginsSurface) : null;
|
|
212
|
+
}
|
|
97
213
|
|
|
98
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Lists plugins. Called with a plugin/handler `context`, returns normalized
|
|
216
|
+
* presence records `{ id, status, enabled }` resolved through the daemon (see
|
|
217
|
+
* `listPluginPresence`); absence tolerates to `[]`. Called without a context,
|
|
218
|
+
* returns the legacy full `PaseoPluginInfo[]` metadata list.
|
|
219
|
+
*/
|
|
220
|
+
export function listPlugins(
|
|
221
|
+
context: PluginRegistryContext | null | undefined,
|
|
222
|
+
options?: PluginQueryOptions,
|
|
223
|
+
): Promise<PluginPresence[]>;
|
|
224
|
+
export function listPlugins(options?: ListPluginsOptions): Promise<PaseoPluginInfo[]>;
|
|
225
|
+
export async function listPlugins(
|
|
226
|
+
contextOrOptions?: PluginRegistryContext | ListPluginsOptions | null,
|
|
227
|
+
maybeOptions: PluginQueryOptions = {},
|
|
228
|
+
): Promise<PluginPresence[] | PaseoPluginInfo[]> {
|
|
229
|
+
// A registry context is an object carrying `paseo`, or an explicitly passed
|
|
230
|
+
// null context (the "caller has no context" case, which tolerates to []). A
|
|
231
|
+
// bare option object (no `paseo` key), or no argument at all, stays on the
|
|
232
|
+
// legacy full-metadata path.
|
|
233
|
+
if (isRegistryContext(contextOrOptions) || contextOrOptions === null) {
|
|
234
|
+
return listPluginPresence(contextOrOptions, maybeOptions);
|
|
235
|
+
}
|
|
236
|
+
return listPluginInfo(contextOrOptions ?? {});
|
|
99
237
|
}
|
|
100
238
|
|
|
101
239
|
function applyFilter(plugins: PaseoPluginInfo[], filter: PluginStatusFilter): PaseoPluginInfo[] {
|
|
@@ -128,13 +266,40 @@ export async function getPluginInfo(
|
|
|
128
266
|
|
|
129
267
|
/**
|
|
130
268
|
* Checks whether a plugin is installed in Paseo.
|
|
269
|
+
*
|
|
270
|
+
* Two forms:
|
|
271
|
+
* - `isPluginInstalled(pluginId, options?)` — legacy metadata query: true when
|
|
272
|
+
* the plugin appears in the full plugin list at all.
|
|
273
|
+
* - `isPluginInstalled(context, id, options?)` — presence query: true only when
|
|
274
|
+
* the plugin is usable (present, enabled, and status "running"). Anything else
|
|
275
|
+
* — absent, disabled, failed, unknown — resolves to false. A missing daemon
|
|
276
|
+
* surface tolerates to false, never throws.
|
|
131
277
|
*/
|
|
132
|
-
export
|
|
278
|
+
export function isPluginInstalled(
|
|
133
279
|
pluginId: string,
|
|
134
|
-
options?:
|
|
280
|
+
options?: PluginQueryOptions,
|
|
281
|
+
): Promise<boolean>;
|
|
282
|
+
export function isPluginInstalled(
|
|
283
|
+
context: PluginRegistryContext | null | undefined,
|
|
284
|
+
id: string,
|
|
285
|
+
options?: PluginQueryOptions,
|
|
286
|
+
): Promise<boolean>;
|
|
287
|
+
export async function isPluginInstalled(
|
|
288
|
+
contextOrId: PluginRegistryContext | string | null | undefined,
|
|
289
|
+
idOrOptions?: string | PluginQueryOptions,
|
|
290
|
+
maybeOptions: PluginQueryOptions = {},
|
|
135
291
|
): Promise<boolean> {
|
|
136
|
-
|
|
137
|
-
|
|
292
|
+
if (typeof contextOrId === "string") {
|
|
293
|
+
const info = await getPluginInfo(contextOrId, idOrOptions as PluginQueryOptions | undefined);
|
|
294
|
+
return info !== null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const id = typeof idOrOptions === "string" ? idOrOptions : "";
|
|
298
|
+
if (!id) return false;
|
|
299
|
+
|
|
300
|
+
const plugins = await listPluginPresence(contextOrId, maybeOptions);
|
|
301
|
+
const match = plugins.find((p) => p.id === id);
|
|
302
|
+
return !!match && match.enabled && match.status === "running";
|
|
138
303
|
}
|
|
139
304
|
|
|
140
305
|
/**
|