@xpufx/paseo-slash 0.1.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/client/commands.ts +122 -0
- package/client/console.tsx +484 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/docs/screenshots/slash-autocomplete.jpg +0 -0
- package/docs/screenshots/slash-console.jpg +0 -0
- package/package.json +27 -0
- package/paseo-plugin.json +4 -0
- package/server/orchestrate.ts +81 -0
- package/server/resources.ts +132 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/resources.ts +289 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { HostToast } from "../host";
|
|
2
|
+
import { getOptionalClientHost } from "../host";
|
|
3
|
+
|
|
4
|
+
export interface CopyToClipboardOptions {
|
|
5
|
+
toast?: HostToast;
|
|
6
|
+
toastMessage?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ClipboardTier = "navigator" | "host" | "rnAsync" | "rnSync" | "execCommand";
|
|
10
|
+
|
|
11
|
+
export interface ClipboardEnvironment {
|
|
12
|
+
hasNavigatorClipboard: boolean;
|
|
13
|
+
hasHostCopyText: boolean;
|
|
14
|
+
hasRnClipboard: boolean;
|
|
15
|
+
hasRnSetStringAsync: boolean;
|
|
16
|
+
isDom: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic tier order for an explicit copy.
|
|
21
|
+
*
|
|
22
|
+
* Web's `navigator.clipboard.writeText` is the only API that rejects when the
|
|
23
|
+
* clipboard did not change, so it leads. The synchronous
|
|
24
|
+
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
|
+
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
|
+
* item in place while the UI claims success (xpufx-org/paseo#278); it is only
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. In a DOM
|
|
28
|
+
* the checked `execCommand` fallback is preferred to that unverifiable path.
|
|
29
|
+
*/
|
|
30
|
+
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
|
+
const tiers: ClipboardTier[] = [];
|
|
32
|
+
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
|
+
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
+
if (env.hasRnSetStringAsync) {
|
|
35
|
+
tiers.push("rnAsync");
|
|
36
|
+
} else if (env.hasRnClipboard && !env.isDom) {
|
|
37
|
+
tiers.push("rnSync");
|
|
38
|
+
}
|
|
39
|
+
tiers.push("execCommand");
|
|
40
|
+
return tiers;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Robust cross-platform clipboard copy helper for Paseo plugins.
|
|
45
|
+
* Works seamlessly across React Native (mobile), web, and desktop.
|
|
46
|
+
*
|
|
47
|
+
* Tier order comes from `clipboardTierOrder`; every tier reports failure
|
|
48
|
+
* honestly so a denied or blocked write never leaves the previous clipboard
|
|
49
|
+
* item behind under a fake success.
|
|
50
|
+
*/
|
|
51
|
+
export async function copyToClipboard(
|
|
52
|
+
text: string,
|
|
53
|
+
options?: CopyToClipboardOptions,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
if (text === null || text === undefined) return false;
|
|
56
|
+
const str = String(text);
|
|
57
|
+
|
|
58
|
+
const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
|
|
59
|
+
|
|
60
|
+
let rn: any;
|
|
61
|
+
try {
|
|
62
|
+
rn = require("react-native");
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore require error if not in RN context
|
|
65
|
+
}
|
|
66
|
+
const rnClipboard = rn?.Clipboard;
|
|
67
|
+
const rnSetStringAsync =
|
|
68
|
+
typeof rnClipboard?.setStringAsync === "function" ? rnClipboard.setStringAsync : undefined;
|
|
69
|
+
const rnSetString =
|
|
70
|
+
typeof rnClipboard?.setString === "function" ? rnClipboard.setString : undefined;
|
|
71
|
+
const copyText = getOptionalClientHost()?.copyText;
|
|
72
|
+
|
|
73
|
+
const isDom =
|
|
74
|
+
typeof globalObj.document !== "undefined" &&
|
|
75
|
+
typeof globalObj.document?.createElement === "function";
|
|
76
|
+
|
|
77
|
+
const tiers = clipboardTierOrder({
|
|
78
|
+
hasNavigatorClipboard: Boolean(globalObj.navigator?.clipboard?.writeText),
|
|
79
|
+
hasHostCopyText: Boolean(copyText),
|
|
80
|
+
hasRnClipboard: Boolean(rnSetStringAsync ?? rnSetString),
|
|
81
|
+
hasRnSetStringAsync: Boolean(rnSetStringAsync),
|
|
82
|
+
isDom,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let success = false;
|
|
86
|
+
for (const tier of tiers) {
|
|
87
|
+
if (success) break;
|
|
88
|
+
try {
|
|
89
|
+
switch (tier) {
|
|
90
|
+
case "navigator":
|
|
91
|
+
await globalObj.navigator.clipboard.writeText(str);
|
|
92
|
+
success = true;
|
|
93
|
+
break;
|
|
94
|
+
case "host":
|
|
95
|
+
if (copyText) {
|
|
96
|
+
await copyText(str);
|
|
97
|
+
success = true;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "rnAsync":
|
|
101
|
+
// Expo's async API returns a boolean; a rejected promise falls through.
|
|
102
|
+
success = (await rnSetStringAsync(str)) !== false;
|
|
103
|
+
break;
|
|
104
|
+
case "rnSync":
|
|
105
|
+
// Only reached off-DOM, where this is the native platform clipboard.
|
|
106
|
+
rnSetString(str);
|
|
107
|
+
success = true;
|
|
108
|
+
break;
|
|
109
|
+
case "execCommand": {
|
|
110
|
+
const doc = globalObj.document;
|
|
111
|
+
if (doc?.createElement && doc?.body) {
|
|
112
|
+
const textarea = doc.createElement("textarea");
|
|
113
|
+
textarea.value = str;
|
|
114
|
+
textarea.style.position = "fixed";
|
|
115
|
+
textarea.style.opacity = "0";
|
|
116
|
+
textarea.style.left = "-9999px";
|
|
117
|
+
doc.body.appendChild(textarea);
|
|
118
|
+
textarea.focus();
|
|
119
|
+
textarea.select();
|
|
120
|
+
const res = doc.execCommand("copy");
|
|
121
|
+
doc.body.removeChild(textarea);
|
|
122
|
+
success = res === true;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Tier failed; try the next one.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (success && options?.toast) {
|
|
133
|
+
try {
|
|
134
|
+
const toastAny = options.toast as any;
|
|
135
|
+
if (typeof toastAny.copied === "function") {
|
|
136
|
+
toastAny.copied(options.toastMessage);
|
|
137
|
+
} else if (typeof toastAny.show === "function") {
|
|
138
|
+
const msg = options.toastMessage
|
|
139
|
+
? `Copied ${options.toastMessage} to clipboard`
|
|
140
|
+
: "Copied to clipboard";
|
|
141
|
+
toastAny.show(msg, { variant: "success" });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore toast failure
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return success;
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type HapticFeedbackType =
|
|
2
|
+
| "light"
|
|
3
|
+
| "medium"
|
|
4
|
+
| "heavy"
|
|
5
|
+
| "success"
|
|
6
|
+
| "warning"
|
|
7
|
+
| "error";
|
|
8
|
+
|
|
9
|
+
const PATTERNS: Record<HapticFeedbackType, number | number[]> = {
|
|
10
|
+
light: 10,
|
|
11
|
+
medium: 25,
|
|
12
|
+
heavy: 45,
|
|
13
|
+
success: [15, 40, 20],
|
|
14
|
+
warning: [30, 50, 30],
|
|
15
|
+
error: [40, 60, 40, 60, 40],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cross-platform haptic feedback helper for Paseo plugins.
|
|
20
|
+
* Supports web vibration API and graceful fallback when vibration is unavailable.
|
|
21
|
+
*/
|
|
22
|
+
export function triggerHaptic(type: HapticFeedbackType = "light"): boolean {
|
|
23
|
+
try {
|
|
24
|
+
const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
|
|
25
|
+
const nav = globalObj.navigator;
|
|
26
|
+
if (typeof nav?.vibrate === "function") {
|
|
27
|
+
const pattern = PATTERNS[type] ?? 15;
|
|
28
|
+
return Boolean(nav.vibrate(pattern));
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore environments where vibration permissions are denied or unavailable
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xpufx/paseo-slash",
|
|
3
|
+
"private": false,
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"test": "vitest run --passWithNoTests"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"paseo-plugin.json",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"client",
|
|
15
|
+
"server",
|
|
16
|
+
"shared",
|
|
17
|
+
"docs",
|
|
18
|
+
"!**/*.test.ts",
|
|
19
|
+
"!**/*.test.tsx",
|
|
20
|
+
"!**/*.test.mjs",
|
|
21
|
+
"!**/.DS_Store"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/xpufx/paseo.git"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_HOOK_URL = "http://127.0.0.1:8099";
|
|
6
|
+
|
|
7
|
+
const HOOK_TIMEOUT_MS = 10_000;
|
|
8
|
+
|
|
9
|
+
function resolveHookUrl(): string {
|
|
10
|
+
return process.env.PASEO_FORGEJO_HOOK_URL?.trim() || DEFAULT_HOOK_URL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveSecretFile(): string {
|
|
14
|
+
const override = process.env.PASEO_FORGEJO_HOOK_SECRET_FILE?.trim();
|
|
15
|
+
if (override) return override;
|
|
16
|
+
return join(process.env.HOME || homedir(), ".paseo", "forgejo-hook.secret");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readSecret(): string {
|
|
20
|
+
const file = resolveSecretFile();
|
|
21
|
+
try {
|
|
22
|
+
const secret = readFileSync(file, "utf8").trim();
|
|
23
|
+
if (secret) return secret;
|
|
24
|
+
} catch {
|
|
25
|
+
// Missing or unreadable file: fall back to the hook's own env convention.
|
|
26
|
+
}
|
|
27
|
+
const fromEnv = process.env.FORGEJO_WEBHOOK_SECRET?.trim();
|
|
28
|
+
if (fromEnv) return fromEnv;
|
|
29
|
+
throw new Error(`forgejo hook secret unavailable at ${file}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorReason(status: number, statusText: string, payload: unknown): string {
|
|
33
|
+
if (payload && typeof payload === "object" && "error" in payload) {
|
|
34
|
+
const reason = (payload as { error?: unknown }).error;
|
|
35
|
+
if (typeof reason === "string" && reason.trim()) return reason.trim();
|
|
36
|
+
}
|
|
37
|
+
if (typeof payload === "string" && payload.trim()) return payload.trim();
|
|
38
|
+
return statusText || `hook refused the handover (${status})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic orchestrator handover: POSTs the caller's agent id to the
|
|
43
|
+
* forgejo hook control endpoint. Fails closed on missing secret, an
|
|
44
|
+
* unreachable hook, or a non-2xx rejection; the secret is never surfaced.
|
|
45
|
+
*/
|
|
46
|
+
export async function orchestrateHandover(agentId: string): Promise<unknown> {
|
|
47
|
+
const id = agentId?.trim();
|
|
48
|
+
if (!id) throw new Error("orchestrate requires a caller agent id");
|
|
49
|
+
|
|
50
|
+
const secret = readSecret();
|
|
51
|
+
const url = `${resolveHookUrl().replace(/\/+$/, "")}/orchestrate`;
|
|
52
|
+
|
|
53
|
+
let response: Response;
|
|
54
|
+
try {
|
|
55
|
+
response = await fetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
authorization: `Bearer ${secret}`,
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({ agentId: id }),
|
|
62
|
+
signal: AbortSignal.timeout(HOOK_TIMEOUT_MS),
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
66
|
+
throw new Error(`orchestrate failed: forgejo hook unreachable at ${url} (${detail})`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const text = await response.text().catch(() => "");
|
|
70
|
+
let payload: unknown = null;
|
|
71
|
+
try {
|
|
72
|
+
payload = text ? JSON.parse(text) : null;
|
|
73
|
+
} catch {
|
|
74
|
+
payload = text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`orchestrate rejected (${response.status}): ${errorReason(response.status, response.statusText, payload)}`);
|
|
79
|
+
}
|
|
80
|
+
return payload ?? { ok: true };
|
|
81
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { PluginStorage, createPluginLogger } from "paseo-plugin-helper/server";
|
|
2
|
+
import {
|
|
3
|
+
KNOWN_OPEN_TARGETS,
|
|
4
|
+
SEED_COMMANDS,
|
|
5
|
+
interpolateTemplate,
|
|
6
|
+
slashSettingsContract,
|
|
7
|
+
type CommandBundle,
|
|
8
|
+
type SlashCommand,
|
|
9
|
+
type SlashSettings,
|
|
10
|
+
} from "../shared/resources";
|
|
11
|
+
import { PLUGIN_VERSION as SLASH_PLUGIN_VERSION } from "../shared/version";
|
|
12
|
+
import { orchestrateHandover } from "./orchestrate";
|
|
13
|
+
|
|
14
|
+
export const log = createPluginLogger("slash", { version: SLASH_PLUGIN_VERSION });
|
|
15
|
+
|
|
16
|
+
const DEFAULTS: SlashSettings = { prefix: "slash-", commands: SEED_COMMANDS };
|
|
17
|
+
|
|
18
|
+
const settingsStorage = new PluginStorage<SlashSettings>("slash", "settings.json", {
|
|
19
|
+
schema: slashSettingsContract.schema,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function readSettings(): Promise<SlashSettings> {
|
|
23
|
+
const data = await settingsStorage.readAsync();
|
|
24
|
+
if (!data.commands || data.commands.length === 0) {
|
|
25
|
+
return { ...data, commands: SEED_COMMANDS };
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleGetSettings(): Promise<SlashSettings> {
|
|
31
|
+
return readSettings();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleUpdateSettings(patch: Partial<SlashSettings>): Promise<SlashSettings> {
|
|
35
|
+
if (patch.prefix !== undefined && !/^[a-z0-9-]*$/.test(patch.prefix)) {
|
|
36
|
+
throw new Error("prefix may only contain lowercase letters, digits, and dashes");
|
|
37
|
+
}
|
|
38
|
+
return settingsStorage.updateAsync((prev) => ({ ...prev, ...patch }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleResetSettings(): Promise<SlashSettings> {
|
|
42
|
+
settingsStorage.reset();
|
|
43
|
+
return DEFAULTS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleListCommands(): Promise<{ commands: SlashCommand[] }> {
|
|
47
|
+
const settings = await readSettings();
|
|
48
|
+
return { commands: settings.commands.filter((c) => c.enabled) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleListCatalog(): { commands: SlashCommand[] } {
|
|
52
|
+
return { commands: SEED_COMMANDS };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function handleListOperations(): { rpc: string[]; open: string[] } {
|
|
56
|
+
return { rpc: allowedOperations(), open: [...KNOWN_OPEN_TARGETS] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface OperationContext {
|
|
60
|
+
agentId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type OperationHandler = (params: Record<string, unknown>, context: OperationContext) => unknown;
|
|
64
|
+
|
|
65
|
+
const SAFE_OPERATIONS: Record<string, OperationHandler> = {
|
|
66
|
+
"slash.ping": () => ({ ok: true, version: SLASH_PLUGIN_VERSION }),
|
|
67
|
+
"slash.echo": (params) => ({ echo: params }),
|
|
68
|
+
"slash.orchestrate": (_params, context) => {
|
|
69
|
+
if (!context.agentId) throw new Error("orchestrate requires a caller agent id");
|
|
70
|
+
return orchestrateHandover(context.agentId);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function allowedOperations(): string[] {
|
|
75
|
+
return Object.keys(SAFE_OPERATIONS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runOperation(
|
|
79
|
+
operation: string,
|
|
80
|
+
params: Record<string, unknown>,
|
|
81
|
+
context: OperationContext = {},
|
|
82
|
+
): Promise<unknown> {
|
|
83
|
+
const handler = SAFE_OPERATIONS[operation];
|
|
84
|
+
if (!handler) {
|
|
85
|
+
throw new Error(`rpc operation not allowlisted: ${operation}`);
|
|
86
|
+
}
|
|
87
|
+
return handler(params, context);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleRunCommand(input: { name: string; args: string; agentId?: string }) {
|
|
91
|
+
const settings = await readSettings();
|
|
92
|
+
const command = settings.commands.find((c) => c.enabled && c.name === input.name);
|
|
93
|
+
if (!command) {
|
|
94
|
+
throw new Error(`unknown or disabled command: ${input.name}`);
|
|
95
|
+
}
|
|
96
|
+
if (command.action.verb === "send") {
|
|
97
|
+
return { verb: "send" as const, prompt: interpolateTemplate(command.action.template, input.args) };
|
|
98
|
+
}
|
|
99
|
+
if (command.action.verb === "open") {
|
|
100
|
+
return { verb: "open" as const, target: command.action.target };
|
|
101
|
+
}
|
|
102
|
+
const result = await runOperation(command.action.operation, command.action.params ?? {}, {
|
|
103
|
+
agentId: input.agentId,
|
|
104
|
+
});
|
|
105
|
+
return { verb: "rpc" as const, result };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function handleExportBundle(): Promise<{ bundle: CommandBundle }> {
|
|
109
|
+
const settings = await readSettings();
|
|
110
|
+
return {
|
|
111
|
+
bundle: {
|
|
112
|
+
bundle: "slash-commands",
|
|
113
|
+
version: 1,
|
|
114
|
+
commands: settings.commands.filter((c) => c.enabled),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function handleImportBundle(input: {
|
|
120
|
+
bundle: CommandBundle;
|
|
121
|
+
overwrite: boolean;
|
|
122
|
+
}): Promise<{ commands: SlashCommand[] }> {
|
|
123
|
+
const settings = await readSettings();
|
|
124
|
+
const existing = new Map(settings.commands.map((c) => [c.name, c]));
|
|
125
|
+
for (const incoming of input.bundle.commands) {
|
|
126
|
+
if (existing.has(incoming.name) && !input.overwrite) continue;
|
|
127
|
+
existing.set(incoming.name, incoming);
|
|
128
|
+
}
|
|
129
|
+
const commands = [...existing.values()];
|
|
130
|
+
await settingsStorage.updateAsync((prev) => ({ ...prev, commands }));
|
|
131
|
+
return { commands };
|
|
132
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { safeExec } from "./process";
|
|
2
|
+
|
|
3
|
+
export interface AgentIdentity {
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
provider?: string;
|
|
8
|
+
repo?: string;
|
|
9
|
+
branch?: string;
|
|
10
|
+
envelopeText?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentIdentityOptions {
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
envelopeCommand?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ENV_KEYS = [
|
|
19
|
+
"AGENT_ID",
|
|
20
|
+
"AGENT_NAME",
|
|
21
|
+
"AGENT_MODEL",
|
|
22
|
+
"AGENT_PROVIDER",
|
|
23
|
+
"PASEO_AGENT_ID",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function readEnvIdentity(env: NodeJS.ProcessEnv = process.env): AgentIdentity | null {
|
|
27
|
+
const id = env.PASEO_AGENT_ID || env.AGENT_ID || undefined;
|
|
28
|
+
const name = env.AGENT_NAME || undefined;
|
|
29
|
+
const model = env.AGENT_MODEL || undefined;
|
|
30
|
+
const provider = env.AGENT_PROVIDER || undefined;
|
|
31
|
+
if (!id && !name && !model && !provider) return null;
|
|
32
|
+
return { id, name, model, provider };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeEnvelopeJson(raw: unknown): AgentIdentity | null {
|
|
36
|
+
if (!raw || typeof raw !== "object") return null;
|
|
37
|
+
const o = raw as Record<string, unknown>;
|
|
38
|
+
const str = (v: unknown): string | undefined =>
|
|
39
|
+
typeof v === "string" && v.length > 0 ? v : undefined;
|
|
40
|
+
const identity: AgentIdentity = {
|
|
41
|
+
id: str(o.id) ?? str(o.agentId),
|
|
42
|
+
name: str(o.name) ?? str(o.agentName),
|
|
43
|
+
model: str(o.model),
|
|
44
|
+
provider: str(o.provider),
|
|
45
|
+
repo: str(o.repo) ?? str(o.workspace),
|
|
46
|
+
branch: str(o.branch),
|
|
47
|
+
envelopeText: str(o.envelopeText) ?? str(o.envelope),
|
|
48
|
+
};
|
|
49
|
+
if (!identity.id && !identity.name && !identity.model && !identity.provider && !identity.repo && !identity.branch) return null;
|
|
50
|
+
return identity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getAgentIdentity(
|
|
54
|
+
options: AgentIdentityOptions = {},
|
|
55
|
+
): Promise<AgentIdentity | null> {
|
|
56
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
57
|
+
const command = options.envelopeCommand ?? "xpufx-tool envelope --format json";
|
|
58
|
+
const envIdentity = readEnvIdentity();
|
|
59
|
+
void ENV_KEYS;
|
|
60
|
+
try {
|
|
61
|
+
const result = await safeExec(command, { timeoutMs });
|
|
62
|
+
if (result.code !== 0) return envIdentity;
|
|
63
|
+
const text = result.stdout.trim();
|
|
64
|
+
if (!text) return envIdentity;
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(text);
|
|
67
|
+
const cliIdentity = normalizeEnvelopeJson(parsed);
|
|
68
|
+
if (!cliIdentity) return envIdentity;
|
|
69
|
+
if (!envIdentity) return cliIdentity;
|
|
70
|
+
return {
|
|
71
|
+
id: envIdentity.id ?? cliIdentity.id,
|
|
72
|
+
name: envIdentity.name ?? cliIdentity.name,
|
|
73
|
+
model: envIdentity.model ?? cliIdentity.model,
|
|
74
|
+
provider: envIdentity.provider ?? cliIdentity.provider,
|
|
75
|
+
repo: cliIdentity.repo,
|
|
76
|
+
branch: cliIdentity.branch,
|
|
77
|
+
envelopeText: cliIdentity.envelopeText,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return envIdentity;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return envIdentity;
|
|
84
|
+
}
|
|
85
|
+
}
|