@omercnet/paseo-omp 0.2.1-next.72.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 +120 -0
- package/SUPPORT.md +42 -0
- package/TESTING.md +150 -0
- package/client/composer-pill-settings.tsx +157 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/mcp-authorization.tsx +168 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +76 -0
- package/client/memory-popover.tsx +74 -0
- package/client/omp-config-surface.tsx +1433 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +1004 -0
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/provider-diagnostics-state.ts +262 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +155 -0
- package/client/quota-state.ts +140 -0
- package/client/sessions-popover.tsx +78 -0
- package/docs/alpha-release-checklist.md +68 -0
- package/docs/configuration.md +126 -0
- package/docs/core-provider-issue-audit.md +108 -0
- 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 +67 -0
- package/index.client.tsx +488 -0
- package/index.server.ts +81 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/scripts/prepare-dependencies.mjs +20 -0
- package/server/hub.ts +145 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +135 -0
- package/server/omp-plugins.ts +676 -0
- package/server/omp-settings.ts +499 -0
- package/server/paths.ts +181 -0
- package/server/provider/catalog.ts +172 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +1196 -0
- package/server/provider/host-tools.ts +777 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2806 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +162 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +736 -0
- package/server/provider/session.ts +4796 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +850 -0
- package/server/provider/timeline-projector.ts +1801 -0
- package/server/provider-diagnostics.ts +1143 -0
- package/server/quota.ts +55 -0
- package/server/sessions.ts +58 -0
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/hub.ts +43 -0
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +24 -0
- package/shared/omp-config.ts +85 -0
- package/shared/omp-plugins.ts +264 -0
- package/shared/omp-settings.ts +214 -0
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +126 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +23 -0
- package/shared/sessions.ts +24 -0
- package/tsconfig.json +16 -0
package/server/quota.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { listOmpQuotas, OmpQuota } from "../shared/quota";
|
|
6
|
+
import { ompDataDir } from "./paths";
|
|
7
|
+
|
|
8
|
+
const QuotaRowSchema = z.object({
|
|
9
|
+
provider: z.string(),
|
|
10
|
+
label: z.string(),
|
|
11
|
+
windowLabel: z.string().nullable(),
|
|
12
|
+
usedFraction: z.number().min(0).nullable(),
|
|
13
|
+
status: z.string().nullable(),
|
|
14
|
+
resetsAt: z.number().int().nullable(),
|
|
15
|
+
recordedAt: z.number().int(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function listOmpQuotasFrom(path: string): OmpQuota[] {
|
|
19
|
+
try {
|
|
20
|
+
const database = new DatabaseSync(path, { readOnly: true, timeout: 500 });
|
|
21
|
+
try {
|
|
22
|
+
const rows = database
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT provider, label, window_label AS windowLabel, used_fraction AS usedFraction,
|
|
25
|
+
status, resets_at AS resetsAt, recorded_at AS recordedAt
|
|
26
|
+
FROM (
|
|
27
|
+
SELECT provider, account_key, limit_id, label, window_label, used_fraction, status,
|
|
28
|
+
resets_at, recorded_at, id,
|
|
29
|
+
ROW_NUMBER() OVER (
|
|
30
|
+
PARTITION BY provider, account_key, limit_id
|
|
31
|
+
ORDER BY recorded_at DESC, id DESC
|
|
32
|
+
) AS position
|
|
33
|
+
FROM usage_history
|
|
34
|
+
)
|
|
35
|
+
WHERE position = 1
|
|
36
|
+
ORDER BY COALESCE(usedFraction, -1) DESC, provider, label`,
|
|
37
|
+
)
|
|
38
|
+
.all();
|
|
39
|
+
return rows.flatMap((row) => {
|
|
40
|
+
const parsed = QuotaRowSchema.safeParse(row);
|
|
41
|
+
return parsed.success ? [parsed.data] : [];
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
database.close();
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveListOmpQuotas(_input: RpcInput<typeof listOmpQuotas>): {
|
|
52
|
+
quotas: OmpQuota[];
|
|
53
|
+
} {
|
|
54
|
+
return { quotas: listOmpQuotasFrom(join(ompDataDir(), "agent.db")) };
|
|
55
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { listOmpSessions, OmpSessionEntry } from "../shared/sessions";
|
|
6
|
+
import { ompDataDir } from "./paths";
|
|
7
|
+
|
|
8
|
+
const PROMPT_LIMIT = 400;
|
|
9
|
+
const ROW_LIMIT = 100;
|
|
10
|
+
|
|
11
|
+
const SessionRowSchema = z.object({
|
|
12
|
+
id: z.number().int(),
|
|
13
|
+
sessionId: z.string().nullable(),
|
|
14
|
+
title: z.string().nullable(),
|
|
15
|
+
prompt: z.string(),
|
|
16
|
+
createdAt: z.number().int(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function listOmpSessionsFrom(path: string, cwd: string): OmpSessionEntry[] {
|
|
20
|
+
try {
|
|
21
|
+
const database = new DatabaseSync(path, { readOnly: true, timeout: 500 });
|
|
22
|
+
try {
|
|
23
|
+
const rows = database
|
|
24
|
+
.prepare(
|
|
25
|
+
`SELECT h.id AS id, h.session_id AS sessionId, t.title AS title,
|
|
26
|
+
h.prompt AS prompt, h.created_at AS createdAt
|
|
27
|
+
FROM history h
|
|
28
|
+
LEFT JOIN session_titles t ON t.session_id = h.session_id
|
|
29
|
+
WHERE h.cwd = ?
|
|
30
|
+
ORDER BY h.created_at DESC, h.id DESC
|
|
31
|
+
LIMIT ?`,
|
|
32
|
+
)
|
|
33
|
+
.all(cwd, ROW_LIMIT);
|
|
34
|
+
return rows.flatMap((row) => {
|
|
35
|
+
const parsed = SessionRowSchema.safeParse(row);
|
|
36
|
+
if (!parsed.success) return [];
|
|
37
|
+
const truncated = parsed.data.prompt.length > PROMPT_LIMIT;
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
...parsed.data,
|
|
41
|
+
prompt: truncated ? parsed.data.prompt.slice(0, PROMPT_LIMIT) : parsed.data.prompt,
|
|
42
|
+
truncated,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
});
|
|
46
|
+
} finally {
|
|
47
|
+
database.close();
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveListOmpSessions({ cwd }: RpcInput<typeof listOmpSessions>): {
|
|
55
|
+
sessions: OmpSessionEntry[];
|
|
56
|
+
} {
|
|
57
|
+
return { sessions: listOmpSessionsFrom(join(ompDataDir(), "history.db"), cwd) };
|
|
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
|
+
});
|
package/shared/hub.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Mirrors the on-disk shape omp's hub writes under
|
|
5
|
+
// ~/.omp/run/daemons/<projectHash>/daemons/<name>/meta.json. That layout is an internal,
|
|
6
|
+
// unversioned implementation detail of the omp harness, not a published API, so every field
|
|
7
|
+
// here is optional-safe on the server side and this schema is intentionally permissive
|
|
8
|
+
// (state is a free-form string, not a fixed enum) to avoid rejecting shapes we have not seen.
|
|
9
|
+
export const HubProcessSchema = z.object({
|
|
10
|
+
name: z.string(),
|
|
11
|
+
application: z.string(),
|
|
12
|
+
args: z.array(z.string()),
|
|
13
|
+
cwd: z.string(),
|
|
14
|
+
state: z.string(),
|
|
15
|
+
owner: z.string().nullable(),
|
|
16
|
+
restartCount: z.number().int().nonnegative(),
|
|
17
|
+
persist: z.boolean(),
|
|
18
|
+
detached: z.boolean(),
|
|
19
|
+
createdAt: z.number().nullable(),
|
|
20
|
+
startedAt: z.number().nullable(),
|
|
21
|
+
readyAt: z.number().nullable(),
|
|
22
|
+
exitedAt: z.number().nullable(),
|
|
23
|
+
exitCode: z.number().nullable(),
|
|
24
|
+
});
|
|
25
|
+
export type HubProcess = z.infer<typeof HubProcessSchema>;
|
|
26
|
+
export const OmpWorkspaceCwdSchema = z.string().min(1).max(4_096);
|
|
27
|
+
const ProcessNameSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(128)
|
|
31
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
32
|
+
|
|
33
|
+
export const listHubProcesses = defineRpc({
|
|
34
|
+
name: "paseo-omp.list-processes",
|
|
35
|
+
input: z.object({ cwd: OmpWorkspaceCwdSchema }),
|
|
36
|
+
output: z.object({ processes: z.array(HubProcessSchema) }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const tailHubLog = defineRpc({
|
|
40
|
+
name: "paseo-omp.tail-log",
|
|
41
|
+
input: z.object({ cwd: OmpWorkspaceCwdSchema, name: ProcessNameSchema }),
|
|
42
|
+
output: z.object({ content: z.string(), truncated: z.boolean() }),
|
|
43
|
+
});
|
package/shared/mcp.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const OMP_MCP_AUTH_TIMELINE_KIND = "omp-mcp-authorization";
|
|
5
|
+
const OmpMcpAuthorizationUrlSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.max(16_384)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value);
|
|
11
|
+
return (
|
|
12
|
+
(url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password
|
|
13
|
+
);
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}, "Authorization URL must be an HTTP URL without embedded credentials");
|
|
18
|
+
|
|
19
|
+
export const ompMcpAuthorizationTimelineSchema = z.object({
|
|
20
|
+
url: OmpMcpAuthorizationUrlSchema,
|
|
21
|
+
instructions: z
|
|
22
|
+
.string()
|
|
23
|
+
.max(64 * 1024)
|
|
24
|
+
.optional(),
|
|
25
|
+
loopbackCallback: z.boolean(),
|
|
26
|
+
browserAuthorizationToken: z.string().uuid().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type OmpMcpAuthorizationTimeline = z.infer<typeof ompMcpAuthorizationTimelineSchema>;
|
|
30
|
+
export const openOmpMcpAuthorizationInPaseoBrowser = defineRpc({
|
|
31
|
+
name: "paseo-omp.open-mcp-authorization-in-browser",
|
|
32
|
+
input: z.object({ authorizationToken: z.string().uuid() }),
|
|
33
|
+
output: z.object({ opened: z.literal(true) }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const OMP_MCP_SERVER_NAME = /^[a-zA-Z0-9_.:-]{1,100}$/u;
|
|
37
|
+
|
|
38
|
+
export type OmpMcpServerAction = "test" | "reauth" | "enable" | "disable";
|
|
39
|
+
|
|
40
|
+
export function buildOmpMcpServerCommand(
|
|
41
|
+
action: OmpMcpServerAction,
|
|
42
|
+
serverName: string,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
const name = serverName.trim();
|
|
45
|
+
if (!OMP_MCP_SERVER_NAME.test(name)) return;
|
|
46
|
+
return `/mcp ${action} ${name}`;
|
|
47
|
+
}
|
package/shared/memory.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { OmpStoreSchema } from "./omp-store";
|
|
4
|
+
|
|
5
|
+
const CwdSchema = z.string().min(1).max(4_096);
|
|
6
|
+
|
|
7
|
+
export const OmpMemoryFactSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
subject: z.string(),
|
|
10
|
+
predicate: z.string(),
|
|
11
|
+
object: z.string(),
|
|
12
|
+
confidence: z.number().min(0).max(1),
|
|
13
|
+
timestamp: z.string().nullable(),
|
|
14
|
+
});
|
|
15
|
+
export type OmpMemoryFact = z.infer<typeof OmpMemoryFactSchema>;
|
|
16
|
+
|
|
17
|
+
export const listOmpMemory = defineRpc({
|
|
18
|
+
name: "paseo-omp.list-memory",
|
|
19
|
+
input: z.object({ store: OmpStoreSchema.optional(), cwd: CwdSchema }),
|
|
20
|
+
output: z.object({
|
|
21
|
+
bank: z.string().nullable(),
|
|
22
|
+
facts: z.array(OmpMemoryFactSchema),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { OmpWorkspaceCwdSchema } from "./hub";
|
|
4
|
+
import { OmpStoreSchema } from "./omp-store";
|
|
5
|
+
|
|
6
|
+
// Mirrors the safe, non-secret subset of omp's on-disk ~/.omp/agent/config.yml. That file is an
|
|
7
|
+
// internal, unversioned config format owned by the omp harness (source: omp's
|
|
8
|
+
// settings-schema.ts), not a published API, so this is an explicit allowlist rather than a
|
|
9
|
+
// passthrough: every section below has been checked against that schema's `credential: true`
|
|
10
|
+
// markers and carries none. Sections the schema marks as credential-bearing (auth broker
|
|
11
|
+
// tokens, mnemopi/hindsight embedding and LLM API keys, searxng basic-auth, blob-destination
|
|
12
|
+
// headers) are deliberately absent and must stay that way. A field not listed here is never
|
|
13
|
+
// read, rendered, or forwarded across the RPC boundary — server/omp-config.ts parses each
|
|
14
|
+
// section independently and omits it entirely if it fails to match, rather than guessing or
|
|
15
|
+
// widening the schema.
|
|
16
|
+
|
|
17
|
+
export const OmpModelRolesSchema = z.record(z.string(), z.string());
|
|
18
|
+
|
|
19
|
+
export const OmpFallbackChainsSchema = z.record(z.string(), z.array(z.string()));
|
|
20
|
+
|
|
21
|
+
export const OmpThemeSectionSchema = z.object({
|
|
22
|
+
dark: z.string().optional(),
|
|
23
|
+
light: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const OmpMemorySectionSchema = z.object({
|
|
27
|
+
backend: z.enum(["off", "local", "hindsight", "mnemopi", "sharpshooter"]).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const OmpGithubCacheSectionSchema = z.object({
|
|
31
|
+
enabled: z.boolean().optional(),
|
|
32
|
+
softTtlSec: z.number().optional(),
|
|
33
|
+
hardTtlSec: z.number().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const OmpGithubSectionSchema = z.object({
|
|
37
|
+
enabled: z.boolean().optional(),
|
|
38
|
+
cache: OmpGithubCacheSectionSchema.optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const OmpRetrySectionSchema = z.object({
|
|
42
|
+
enabled: z.boolean().optional(),
|
|
43
|
+
maxRetries: z.number().optional(),
|
|
44
|
+
baseDelayMs: z.number().optional(),
|
|
45
|
+
maxDelayMs: z.number().optional(),
|
|
46
|
+
waitForUsageReset: z.boolean().optional(),
|
|
47
|
+
modelFallback: z.boolean().optional(),
|
|
48
|
+
usageAwareFallback: z.boolean().optional(),
|
|
49
|
+
usageReservePct: z.number().optional(),
|
|
50
|
+
usageReservePolicy: z.enum(["confirm", "auto", "fail-closed"]).optional(),
|
|
51
|
+
fallbackRevertPolicy: z.enum(["cooldown-expiry", "never"]).optional(),
|
|
52
|
+
fallbackChains: OmpFallbackChainsSchema.optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const OmpDevSectionSchema = z.object({
|
|
56
|
+
autoqaConsent: z.enum(["unset", "granted", "denied"]).optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const OmpConfigSchema = z.object({
|
|
60
|
+
setupVersion: z.number().optional(),
|
|
61
|
+
symbolPreset: z.enum(["unicode", "nerd", "ascii"]).optional(),
|
|
62
|
+
defaultThinkingLevel: z.string().optional(),
|
|
63
|
+
theme: OmpThemeSectionSchema.optional(),
|
|
64
|
+
memory: OmpMemorySectionSchema.optional(),
|
|
65
|
+
github: OmpGithubSectionSchema.optional(),
|
|
66
|
+
disabledProviders: z.array(z.string()).optional(),
|
|
67
|
+
modelProviderOrder: z.array(z.string()).optional(),
|
|
68
|
+
modelRoles: OmpModelRolesSchema.optional(),
|
|
69
|
+
enabledModels: z.array(z.string()).optional(),
|
|
70
|
+
retry: OmpRetrySectionSchema.optional(),
|
|
71
|
+
dev: OmpDevSectionSchema.optional(),
|
|
72
|
+
});
|
|
73
|
+
export type OmpConfig = z.infer<typeof OmpConfigSchema>;
|
|
74
|
+
|
|
75
|
+
export const listOmpConfig = defineRpc({
|
|
76
|
+
name: "paseo-omp.list-config",
|
|
77
|
+
input: z
|
|
78
|
+
.object({ store: OmpStoreSchema.optional(), cwd: OmpWorkspaceCwdSchema.optional() })
|
|
79
|
+
.strict(),
|
|
80
|
+
output: z.object({
|
|
81
|
+
path: z.string(),
|
|
82
|
+
available: z.boolean(),
|
|
83
|
+
config: OmpConfigSchema.nullable(),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { OmpWorkspaceCwdSchema } from "./hub";
|
|
4
|
+
import { OmpStoreSchema } from "./omp-store";
|
|
5
|
+
|
|
6
|
+
export const OMP_PLUGIN_LIMIT = 256;
|
|
7
|
+
export const OMP_PLUGIN_ARGUMENT_LIMIT = 512;
|
|
8
|
+
|
|
9
|
+
const SAFE_NPM_PACKAGE = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
|
|
10
|
+
const SAFE_MARKETPLACE_ID =
|
|
11
|
+
/^[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?@[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?$/u;
|
|
12
|
+
|
|
13
|
+
function utf8ByteLength(value: string): number {
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
for (const character of value) {
|
|
16
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
17
|
+
bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
18
|
+
}
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasUnsafeArgumentCharacter(value: string): boolean {
|
|
23
|
+
for (const character of value) {
|
|
24
|
+
const code = character.codePointAt(0) ?? 0;
|
|
25
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const OmpPluginNameSchema = z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(214)
|
|
34
|
+
.regex(SAFE_NPM_PACKAGE, "Expected an installed OMP package name");
|
|
35
|
+
|
|
36
|
+
export const OmpMarketplacePluginIdSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.min(3)
|
|
39
|
+
.max(128)
|
|
40
|
+
.regex(SAFE_MARKETPLACE_ID, "Expected name@marketplace");
|
|
41
|
+
|
|
42
|
+
export const OmpPluginTargetSchema = z.union([OmpPluginNameSchema, OmpMarketplacePluginIdSchema]);
|
|
43
|
+
|
|
44
|
+
export const OmpPluginInstallSourceSchema = z
|
|
45
|
+
.string()
|
|
46
|
+
.min(1)
|
|
47
|
+
.max(OMP_PLUGIN_ARGUMENT_LIMIT)
|
|
48
|
+
.refine(
|
|
49
|
+
(value) => utf8ByteLength(value) <= OMP_PLUGIN_ARGUMENT_LIMIT,
|
|
50
|
+
"Plugin source is too large",
|
|
51
|
+
)
|
|
52
|
+
.refine((value) => value.trim() === value, "Plugin source must not have surrounding whitespace")
|
|
53
|
+
.refine(
|
|
54
|
+
(value) => !hasUnsafeArgumentCharacter(value),
|
|
55
|
+
"Plugin source contains an unsafe character",
|
|
56
|
+
)
|
|
57
|
+
.refine((value) => !value.startsWith("-"), "Plugin source must not be an option");
|
|
58
|
+
|
|
59
|
+
export const OmpPluginScopeSchema = z.enum(["user", "project"]);
|
|
60
|
+
export type OmpPluginScope = z.infer<typeof OmpPluginScopeSchema>;
|
|
61
|
+
|
|
62
|
+
export const OmpInstalledPluginSchema = z
|
|
63
|
+
.object({
|
|
64
|
+
id: z.string().min(1).max(214),
|
|
65
|
+
packageName: OmpPluginNameSchema.optional(),
|
|
66
|
+
version: z.string().min(1).max(128).nullable(),
|
|
67
|
+
source: z.enum(["npm", "marketplace"]),
|
|
68
|
+
scope: OmpPluginScopeSchema.nullable(),
|
|
69
|
+
enabled: z.boolean(),
|
|
70
|
+
shadowed: z.boolean(),
|
|
71
|
+
path: z.string().min(1).max(4_096).nullable(),
|
|
72
|
+
description: z.string().max(1_024).nullable(),
|
|
73
|
+
enabledFeatures: z.array(z.string().min(1).max(128)).max(128),
|
|
74
|
+
availableFeatures: z.array(z.string().min(1).max(128)).max(128),
|
|
75
|
+
configurable: z.boolean(),
|
|
76
|
+
ambiguous: z.boolean(),
|
|
77
|
+
configAmbiguous: z.boolean(),
|
|
78
|
+
usesDefaultFeatures: z.boolean(),
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
export type OmpInstalledPlugin = z.infer<typeof OmpInstalledPluginSchema>;
|
|
82
|
+
|
|
83
|
+
export const OmpPluginStateSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
available: z.boolean(),
|
|
86
|
+
plugins: z.array(OmpInstalledPluginSchema).max(OMP_PLUGIN_LIMIT),
|
|
87
|
+
droppedCount: z.number().int().nonnegative(),
|
|
88
|
+
error: z.string().max(256).optional(),
|
|
89
|
+
})
|
|
90
|
+
.strict();
|
|
91
|
+
export type OmpPluginState = z.infer<typeof OmpPluginStateSchema>;
|
|
92
|
+
|
|
93
|
+
export const listOmpPlugins = defineRpc({
|
|
94
|
+
name: "paseo-omp.list-plugins",
|
|
95
|
+
input: z
|
|
96
|
+
.object({ store: OmpStoreSchema.optional(), cwd: OmpWorkspaceCwdSchema.optional() })
|
|
97
|
+
.strict(),
|
|
98
|
+
output: OmpPluginStateSchema,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const OmpPluginConfigSettingSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
key: z.string().min(1).max(128),
|
|
104
|
+
type: z.enum(["string", "number", "boolean", "enum"]),
|
|
105
|
+
description: z.string().max(512),
|
|
106
|
+
configured: z.boolean(),
|
|
107
|
+
secret: z.boolean(),
|
|
108
|
+
enumValues: z.array(z.string().min(1).max(256)).max(128),
|
|
109
|
+
minimum: z.number().finite().optional(),
|
|
110
|
+
maximum: z.number().finite().optional(),
|
|
111
|
+
step: z.number().finite().positive().optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
export type OmpPluginConfigSetting = z.infer<typeof OmpPluginConfigSettingSchema>;
|
|
115
|
+
|
|
116
|
+
export const OmpPluginConfigStateSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
available: z.boolean(),
|
|
119
|
+
plugin: OmpPluginNameSchema,
|
|
120
|
+
settings: z.array(OmpPluginConfigSettingSchema).max(OMP_PLUGIN_LIMIT),
|
|
121
|
+
droppedCount: z.number().int().nonnegative(),
|
|
122
|
+
error: z.string().max(256).optional(),
|
|
123
|
+
})
|
|
124
|
+
.strict();
|
|
125
|
+
export type OmpPluginConfigState = z.infer<typeof OmpPluginConfigStateSchema>;
|
|
126
|
+
|
|
127
|
+
export const inspectOmpPluginConfig = defineRpc({
|
|
128
|
+
name: "paseo-omp.inspect-plugin-config",
|
|
129
|
+
input: z
|
|
130
|
+
.object({
|
|
131
|
+
store: OmpStoreSchema.optional(),
|
|
132
|
+
plugin: OmpPluginNameSchema,
|
|
133
|
+
cwd: OmpWorkspaceCwdSchema.optional(),
|
|
134
|
+
})
|
|
135
|
+
.strict(),
|
|
136
|
+
output: OmpPluginConfigStateSchema,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const OmpPluginConfigKeySchema = z
|
|
140
|
+
.string()
|
|
141
|
+
.min(1)
|
|
142
|
+
.max(128)
|
|
143
|
+
.refine((value) => value.trim() === value, "Setting key must not have surrounding whitespace")
|
|
144
|
+
.refine((value) => !hasUnsafeArgumentCharacter(value), "Setting key contains an unsafe character")
|
|
145
|
+
.refine((value) => !value.startsWith("-"), "Setting key must not be an option");
|
|
146
|
+
|
|
147
|
+
export const OmpPluginConfigStringValueSchema = z
|
|
148
|
+
.string()
|
|
149
|
+
.min(1)
|
|
150
|
+
.max(4_096)
|
|
151
|
+
.refine((value) => utf8ByteLength(value) <= 4_096, "Setting value is too large")
|
|
152
|
+
.refine(
|
|
153
|
+
(value) => !hasUnsafeArgumentCharacter(value),
|
|
154
|
+
"Setting value contains an unsafe character",
|
|
155
|
+
)
|
|
156
|
+
.refine((value) => !value.startsWith("-"), "Setting value must not be an option");
|
|
157
|
+
|
|
158
|
+
export const OmpPluginConfigMutationSchema = z.discriminatedUnion("action", [
|
|
159
|
+
z
|
|
160
|
+
.object({
|
|
161
|
+
action: z.literal("set"),
|
|
162
|
+
plugin: OmpPluginNameSchema,
|
|
163
|
+
key: OmpPluginConfigKeySchema,
|
|
164
|
+
value: z.union([OmpPluginConfigStringValueSchema, z.number().finite(), z.boolean()]),
|
|
165
|
+
store: OmpStoreSchema.optional(),
|
|
166
|
+
cwd: OmpWorkspaceCwdSchema.optional(),
|
|
167
|
+
})
|
|
168
|
+
.strict(),
|
|
169
|
+
z
|
|
170
|
+
.object({
|
|
171
|
+
action: z.literal("delete"),
|
|
172
|
+
plugin: OmpPluginNameSchema,
|
|
173
|
+
key: OmpPluginConfigKeySchema,
|
|
174
|
+
store: OmpStoreSchema.optional(),
|
|
175
|
+
cwd: OmpWorkspaceCwdSchema.optional(),
|
|
176
|
+
})
|
|
177
|
+
.strict(),
|
|
178
|
+
]);
|
|
179
|
+
export type OmpPluginConfigMutation = z.infer<typeof OmpPluginConfigMutationSchema>;
|
|
180
|
+
|
|
181
|
+
export const mutateOmpPluginConfig = defineRpc({
|
|
182
|
+
name: "paseo-omp.mutate-plugin-config",
|
|
183
|
+
input: OmpPluginConfigMutationSchema,
|
|
184
|
+
output: z
|
|
185
|
+
.object({
|
|
186
|
+
ok: z.boolean(),
|
|
187
|
+
message: z.string().min(1).max(256),
|
|
188
|
+
config: OmpPluginConfigStateSchema,
|
|
189
|
+
})
|
|
190
|
+
.strict(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const ScopedMutationShape = {
|
|
194
|
+
scope: OmpPluginScopeSchema.optional(),
|
|
195
|
+
store: OmpStoreSchema.optional(),
|
|
196
|
+
cwd: OmpWorkspaceCwdSchema.optional(),
|
|
197
|
+
};
|
|
198
|
+
export const OmpPluginMutationSchema = z
|
|
199
|
+
.discriminatedUnion("action", [
|
|
200
|
+
z
|
|
201
|
+
.object({
|
|
202
|
+
action: z.literal("install"),
|
|
203
|
+
source: OmpPluginInstallSourceSchema,
|
|
204
|
+
...ScopedMutationShape,
|
|
205
|
+
})
|
|
206
|
+
.strict(),
|
|
207
|
+
z
|
|
208
|
+
.object({
|
|
209
|
+
action: z.literal("enable"),
|
|
210
|
+
plugin: OmpPluginTargetSchema,
|
|
211
|
+
...ScopedMutationShape,
|
|
212
|
+
})
|
|
213
|
+
.strict(),
|
|
214
|
+
z
|
|
215
|
+
.object({
|
|
216
|
+
action: z.literal("disable"),
|
|
217
|
+
plugin: OmpPluginTargetSchema,
|
|
218
|
+
...ScopedMutationShape,
|
|
219
|
+
})
|
|
220
|
+
.strict(),
|
|
221
|
+
z
|
|
222
|
+
.object({
|
|
223
|
+
action: z.literal("uninstall"),
|
|
224
|
+
plugin: OmpPluginTargetSchema,
|
|
225
|
+
...ScopedMutationShape,
|
|
226
|
+
})
|
|
227
|
+
.strict(),
|
|
228
|
+
z
|
|
229
|
+
.object({
|
|
230
|
+
action: z.literal("upgrade"),
|
|
231
|
+
plugin: OmpMarketplacePluginIdSchema,
|
|
232
|
+
...ScopedMutationShape,
|
|
233
|
+
})
|
|
234
|
+
.strict(),
|
|
235
|
+
])
|
|
236
|
+
.superRefine((input, context) => {
|
|
237
|
+
if (input.scope === "project" && input.cwd === undefined) {
|
|
238
|
+
context.addIssue({
|
|
239
|
+
code: "custom",
|
|
240
|
+
message: "Project-scoped plugin actions require a workspace",
|
|
241
|
+
path: ["cwd"],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (input.action === "install" && input.scope === "project") {
|
|
245
|
+
context.addIssue({
|
|
246
|
+
code: "custom",
|
|
247
|
+
message: "Project-scoped installation is not supported through this API",
|
|
248
|
+
path: ["scope"],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
export type OmpPluginMutation = z.infer<typeof OmpPluginMutationSchema>;
|
|
253
|
+
|
|
254
|
+
export const mutateOmpPlugin = defineRpc({
|
|
255
|
+
name: "paseo-omp.mutate-plugin",
|
|
256
|
+
input: OmpPluginMutationSchema,
|
|
257
|
+
output: z
|
|
258
|
+
.object({
|
|
259
|
+
ok: z.boolean(),
|
|
260
|
+
message: z.string().min(1).max(256),
|
|
261
|
+
state: OmpPluginStateSchema,
|
|
262
|
+
})
|
|
263
|
+
.strict(),
|
|
264
|
+
});
|