@earendil-works/pi-radius-work 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.
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
const TOKENS_DIR =
|
|
10
|
+
process.env.GOOGLE_WORKSPACE_TOKENS_DIR ||
|
|
11
|
+
path.join(os.homedir(), ".pi", "google-workspace", "tokens");
|
|
12
|
+
|
|
13
|
+
const GOOGLE_WORKSPACE_BROKER_CALLBACK_URL =
|
|
14
|
+
"https://radius.pi.dev/v1/oauth/google-workspaces/callback";
|
|
15
|
+
|
|
16
|
+
type TokenPayload = {
|
|
17
|
+
access_token: string;
|
|
18
|
+
refresh_token?: string | null;
|
|
19
|
+
expiry_date: number;
|
|
20
|
+
scope?: string;
|
|
21
|
+
token_type?: string;
|
|
22
|
+
__authMode: "broker";
|
|
23
|
+
__email: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function googleWorkspacesExtension(pi: ExtensionAPI) {
|
|
27
|
+
pi.registerCommand("google-workspaces", {
|
|
28
|
+
description: "Connect a Google Workspace account via Radius",
|
|
29
|
+
handler: async (_args: string, ctx: any) => {
|
|
30
|
+
try {
|
|
31
|
+
ctx.ui.setStatus("google-workspaces", "Opening Google Workspace sign-in...");
|
|
32
|
+
const result = await loginGoogleWorkspaceAccount();
|
|
33
|
+
ctx.ui.notify(`Connected Google Workspace account: ${result.email}`, "info");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
ctx.ui.setEditorText(message);
|
|
37
|
+
ctx.ui.notify("Google Workspace login failed", "error");
|
|
38
|
+
} finally {
|
|
39
|
+
ctx.ui.setStatus("google-workspaces", undefined);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loginGoogleWorkspaceAccount(): Promise<{ email: string; tokenPath: string }> {
|
|
46
|
+
ensureTokensDir();
|
|
47
|
+
|
|
48
|
+
const host = process.env.GOOGLE_WORKSPACE_CALLBACK_HOST || "localhost";
|
|
49
|
+
const port = await getAvailablePort();
|
|
50
|
+
const localCallbackUrl = `http://${host}:${port}/oauth2callback`;
|
|
51
|
+
|
|
52
|
+
const startUrl = new URL(getBrokerStartUrl());
|
|
53
|
+
startUrl.searchParams.set("return_to", localCallbackUrl);
|
|
54
|
+
|
|
55
|
+
const token = await waitForOAuthRedirect({
|
|
56
|
+
host,
|
|
57
|
+
port,
|
|
58
|
+
startUrl: startUrl.toString(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const email = normalizeEmail(token.__email);
|
|
62
|
+
const tokenPath = tokenPathForEmail(email);
|
|
63
|
+
fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2));
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(tokenPath, 0o600);
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore chmod failures on non-POSIX filesystems.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { email, tokenPath };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getBrokerStartUrl(): string {
|
|
74
|
+
return GOOGLE_WORKSPACE_BROKER_CALLBACK_URL.endsWith("/callback")
|
|
75
|
+
? `${GOOGLE_WORKSPACE_BROKER_CALLBACK_URL.slice(0, -"/callback".length)}/start`
|
|
76
|
+
: `${GOOGLE_WORKSPACE_BROKER_CALLBACK_URL}/start`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function waitForOAuthRedirect(input: {
|
|
80
|
+
host: string;
|
|
81
|
+
port: number;
|
|
82
|
+
startUrl: string;
|
|
83
|
+
}): Promise<TokenPayload> {
|
|
84
|
+
const { host, port, startUrl } = input;
|
|
85
|
+
|
|
86
|
+
const redirectPromise = new Promise<TokenPayload>((resolve, reject) => {
|
|
87
|
+
const server = http.createServer((req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
if (!req.url || !req.url.startsWith("/oauth2callback")) {
|
|
90
|
+
res.statusCode = 404;
|
|
91
|
+
res.end("Not found");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parsed = new URL(req.url, `http://${host}:${port}`);
|
|
96
|
+
const errorCode = parsed.searchParams.get("error");
|
|
97
|
+
if (errorCode) {
|
|
98
|
+
const description =
|
|
99
|
+
parsed.searchParams.get("error_description") || "No additional details";
|
|
100
|
+
res.statusCode = 400;
|
|
101
|
+
res.end("Authentication failed.");
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
server.close(() => reject(new Error(`Google OAuth error: ${errorCode}. ${description}`)));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const email = parsed.searchParams.get("email");
|
|
108
|
+
const accessToken = parsed.searchParams.get("access_token");
|
|
109
|
+
const refreshToken = parsed.searchParams.get("refresh_token");
|
|
110
|
+
const scope = parsed.searchParams.get("scope");
|
|
111
|
+
const tokenType = parsed.searchParams.get("token_type");
|
|
112
|
+
const expiryDateRaw = parsed.searchParams.get("expiry_date");
|
|
113
|
+
|
|
114
|
+
if (!email || !accessToken || !expiryDateRaw) {
|
|
115
|
+
res.statusCode = 400;
|
|
116
|
+
res.end("Authentication failed: missing callback fields.");
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
server.close(() =>
|
|
119
|
+
reject(new Error("Authentication failed: callback did not include email and tokens.")),
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const expiryDate = Number.parseInt(expiryDateRaw, 10);
|
|
125
|
+
if (Number.isNaN(expiryDate)) {
|
|
126
|
+
res.statusCode = 400;
|
|
127
|
+
res.end("Authentication failed: invalid expiry date.");
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
server.close(() =>
|
|
130
|
+
reject(new Error("Authentication failed: callback expiry_date is invalid.")),
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const token: TokenPayload = {
|
|
136
|
+
access_token: accessToken,
|
|
137
|
+
refresh_token: refreshToken || null,
|
|
138
|
+
scope: scope || undefined,
|
|
139
|
+
token_type: tokenType || undefined,
|
|
140
|
+
expiry_date: expiryDate,
|
|
141
|
+
__authMode: "broker",
|
|
142
|
+
__email: normalizeEmail(email),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
res.end("Authentication successful. You can close this tab.");
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
server.close(() => resolve(token));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
server.close(() => reject(error));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const timer = setTimeout(
|
|
155
|
+
() => {
|
|
156
|
+
server.close(() =>
|
|
157
|
+
reject(new Error("Authentication timed out after 5 minutes. Please try again.")),
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
5 * 60 * 1000,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
server.on("error", (error) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(new Error(`OAuth callback server error: ${error.message}`));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
server.listen(port, host, () => {
|
|
169
|
+
try {
|
|
170
|
+
openUrlInBrowser(startUrl);
|
|
171
|
+
} catch {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
server.close(() =>
|
|
174
|
+
reject(
|
|
175
|
+
new Error(`Could not open browser automatically. Open this URL manually:\n${startUrl}`),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return redirectPromise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeEmail(email: string): string {
|
|
186
|
+
const normalized = String(email || "")
|
|
187
|
+
.trim()
|
|
188
|
+
.toLowerCase();
|
|
189
|
+
if (!normalized) {
|
|
190
|
+
throw new Error("Google Workspace account email is missing.");
|
|
191
|
+
}
|
|
192
|
+
if (!/^[^@\s]+@[^@\s]+$/.test(normalized)) {
|
|
193
|
+
throw new Error(`Invalid email address: ${email}`);
|
|
194
|
+
}
|
|
195
|
+
return normalized;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function tokenFilenameForEmail(email: string): string {
|
|
199
|
+
return `${encodeURIComponent(normalizeEmail(email))}.json`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function tokenPathForEmail(email: string): string {
|
|
203
|
+
return path.join(TOKENS_DIR, tokenFilenameForEmail(email));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ensureTokensDir(): void {
|
|
207
|
+
fs.mkdirSync(TOKENS_DIR, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getAvailablePort(): Promise<number> {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const server = net.createServer();
|
|
213
|
+
server.listen(0, () => {
|
|
214
|
+
const address = server.address();
|
|
215
|
+
const port = address && typeof address === "object" ? address.port : 0;
|
|
216
|
+
server.close(() => resolve(port));
|
|
217
|
+
});
|
|
218
|
+
server.on("error", reject);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function openUrlInBrowser(targetUrl: string): void {
|
|
223
|
+
let command: string;
|
|
224
|
+
let args: string[];
|
|
225
|
+
|
|
226
|
+
if (process.platform === "darwin") {
|
|
227
|
+
command = "open";
|
|
228
|
+
args = [targetUrl];
|
|
229
|
+
} else if (process.platform === "win32") {
|
|
230
|
+
command = "cmd";
|
|
231
|
+
args = ["/c", "start", "", targetUrl];
|
|
232
|
+
} else {
|
|
233
|
+
command = "xdg-open";
|
|
234
|
+
args = [targetUrl];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = spawn(command, args, {
|
|
238
|
+
detached: true,
|
|
239
|
+
stdio: "ignore",
|
|
240
|
+
});
|
|
241
|
+
child.unref();
|
|
242
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Box, Spacer, Text } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
type GatewayImageGenerationConfig = {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type GatewayConfig = {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
models: Array<{ id: string; name: string }>;
|
|
12
|
+
tools?: {
|
|
13
|
+
image_generation?: GatewayImageGenerationConfig;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type GatewayProvider = {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
gateway: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
config: GatewayConfig;
|
|
23
|
+
authenticatedConfig?: GatewayConfig;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ImageGenerationToolInput = {
|
|
27
|
+
prompt: string;
|
|
28
|
+
input_images?: Array<{
|
|
29
|
+
mediaType: string;
|
|
30
|
+
data: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type GatewayImageGenerationResponse = {
|
|
35
|
+
kind: "radius#image_generation";
|
|
36
|
+
model: string;
|
|
37
|
+
prompt: string;
|
|
38
|
+
images: Array<{
|
|
39
|
+
dataUrl: string;
|
|
40
|
+
mediaType: string;
|
|
41
|
+
}>;
|
|
42
|
+
text?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DEFAULT_GATEWAY = "https://radius.pi.dev";
|
|
46
|
+
const DEFAULT_DEV_GATEWAY = "http://localhost:8788";
|
|
47
|
+
const GATEWAY_PROVIDER_ID = "radius";
|
|
48
|
+
const DEV_GATEWAY_PROVIDER_ID = "radius-dev";
|
|
49
|
+
const GATEWAY_API_KEY_ENV = "PI_GATEWAY_API_KEY";
|
|
50
|
+
const DEV_GATEWAY_API_KEY_ENV = "PI_DEV_GATEWAY_API_KEY";
|
|
51
|
+
const IMAGE_GENERATION_TOOL_NAME = "image_generation";
|
|
52
|
+
const IMAGE_GENERATION_MESSAGE_TYPE = "radius-image-generation";
|
|
53
|
+
|
|
54
|
+
export default async function (pi: ExtensionAPI) {
|
|
55
|
+
const providers: GatewayProvider[] = [
|
|
56
|
+
{
|
|
57
|
+
id: GATEWAY_PROVIDER_ID,
|
|
58
|
+
name: "Radius",
|
|
59
|
+
gateway: process.env.PI_GATEWAY || DEFAULT_GATEWAY,
|
|
60
|
+
apiKey: process.env[GATEWAY_API_KEY_ENV],
|
|
61
|
+
config: createInitialGatewayConfig(process.env.PI_GATEWAY || DEFAULT_GATEWAY),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const devGateway = process.env.PI_DEV_GATEWAY || DEFAULT_DEV_GATEWAY;
|
|
66
|
+
if (devGateway) {
|
|
67
|
+
providers.push({
|
|
68
|
+
id: DEV_GATEWAY_PROVIDER_ID,
|
|
69
|
+
name: "Radius (dev)",
|
|
70
|
+
gateway: devGateway,
|
|
71
|
+
apiKey: process.env[DEV_GATEWAY_API_KEY_ENV],
|
|
72
|
+
config: createInitialGatewayConfig(devGateway),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pi.registerMessageRenderer(IMAGE_GENERATION_MESSAGE_TYPE, (message, _options, theme) => {
|
|
77
|
+
const details = message.details as GatewayImageGenerationResponse | undefined;
|
|
78
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
79
|
+
box.addChild(
|
|
80
|
+
new Text(theme.fg("customMessageLabel", "\x1b[1m[image_generation]\x1b[22m"), 0, 0),
|
|
81
|
+
);
|
|
82
|
+
box.addChild(new Spacer(1));
|
|
83
|
+
box.addChild(new Text(theme.fg("customMessageText", message.content as string), 0, 0));
|
|
84
|
+
if (details?.images?.[0]) {
|
|
85
|
+
const first = details.images[0];
|
|
86
|
+
const match = /^data:[^;]+;base64,(.*)$/s.exec(first.dataUrl);
|
|
87
|
+
if (match?.[1]) {
|
|
88
|
+
box.addChild(new Spacer(1));
|
|
89
|
+
box.addChild(
|
|
90
|
+
new Text(theme.fg("customMessageText", `[image attached: ${first.mediaType}]`), 0, 0),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return box;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.registerTool({
|
|
98
|
+
name: IMAGE_GENERATION_TOOL_NAME,
|
|
99
|
+
label: "Image Generation",
|
|
100
|
+
description:
|
|
101
|
+
"Generate or edit an image through Radius using Gemini 2.5 Flash Image via OpenRouter.",
|
|
102
|
+
promptSnippet: "Generate or edit an image through Radius",
|
|
103
|
+
parameters: Type.Object(
|
|
104
|
+
{
|
|
105
|
+
prompt: Type.String({ description: "Image generation prompt" }),
|
|
106
|
+
input_images: Type.Optional(
|
|
107
|
+
Type.Array(
|
|
108
|
+
Type.Object(
|
|
109
|
+
{
|
|
110
|
+
mediaType: Type.String(),
|
|
111
|
+
data: Type.String(),
|
|
112
|
+
},
|
|
113
|
+
{ additionalProperties: false },
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
{ additionalProperties: false },
|
|
119
|
+
),
|
|
120
|
+
async execute(
|
|
121
|
+
_toolCallId: string,
|
|
122
|
+
params: ImageGenerationToolInput,
|
|
123
|
+
signal: AbortSignal | undefined,
|
|
124
|
+
_onUpdate: unknown,
|
|
125
|
+
ctx: any,
|
|
126
|
+
) {
|
|
127
|
+
const provider = selectProvider(providers, ctx);
|
|
128
|
+
const config = getEffectiveConfig(provider).tools?.image_generation;
|
|
129
|
+
if (!config) {
|
|
130
|
+
throw new Error("Radius image generation is not configured");
|
|
131
|
+
}
|
|
132
|
+
const apiKey = await getApiKey(provider, ctx);
|
|
133
|
+
const response = await fetch(config.endpoint, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
authorization: `Bearer ${apiKey}`,
|
|
137
|
+
accept: "application/json",
|
|
138
|
+
"content-type": "application/json",
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
prompt: params.prompt,
|
|
142
|
+
input_images: params.input_images,
|
|
143
|
+
}),
|
|
144
|
+
signal,
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
|
|
148
|
+
}
|
|
149
|
+
const payload = (await response.json()) as GatewayImageGenerationResponse;
|
|
150
|
+
const summary = `Generated ${payload.images.length} image(s) with ${payload.model}.`;
|
|
151
|
+
pi.sendMessage({
|
|
152
|
+
customType: IMAGE_GENERATION_MESSAGE_TYPE,
|
|
153
|
+
content: summary,
|
|
154
|
+
display: true,
|
|
155
|
+
details: payload,
|
|
156
|
+
});
|
|
157
|
+
const content: Array<any> = [{ type: "text", text: JSON.stringify(payload, null, 2) }];
|
|
158
|
+
for (const image of payload.images) {
|
|
159
|
+
const match = /^data:[^;]+;base64,(.*)$/s.exec(image.dataUrl);
|
|
160
|
+
if (match?.[1]) {
|
|
161
|
+
content.push({ type: "image", data: match[1], mimeType: image.mediaType });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
content,
|
|
166
|
+
details: { provider: provider.id, ...payload },
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectProvider(providers: GatewayProvider[], ctx: any): GatewayProvider {
|
|
173
|
+
const provider = providers.find((candidate) => candidate.id === ctx.model?.provider);
|
|
174
|
+
if (!provider) {
|
|
175
|
+
throw new Error("image_generation is only available when the current model is a Radius model");
|
|
176
|
+
}
|
|
177
|
+
return provider;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getApiKey(provider: GatewayProvider, ctx: any): Promise<string> {
|
|
181
|
+
if (provider.apiKey) {
|
|
182
|
+
return provider.apiKey;
|
|
183
|
+
}
|
|
184
|
+
const result = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
185
|
+
if (!result.ok || !result.apiKey) {
|
|
186
|
+
throw new Error(result.ok ? "Missing gateway API key" : result.error);
|
|
187
|
+
}
|
|
188
|
+
return result.apiKey;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getEffectiveConfig(provider?: GatewayProvider): GatewayConfig {
|
|
192
|
+
return (
|
|
193
|
+
provider?.authenticatedConfig ?? provider?.config ?? createInitialGatewayConfig(DEFAULT_GATEWAY)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createInitialGatewayConfig(gateway: string): GatewayConfig {
|
|
198
|
+
return {
|
|
199
|
+
baseUrl: `${gateway.replace(/\/+$/u, "")}/v1`,
|
|
200
|
+
models: [],
|
|
201
|
+
tools: {
|
|
202
|
+
image_generation: {
|
|
203
|
+
endpoint: `${gateway.replace(/\/+$/u, "")}/v1/images/generate`,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@earendil-works/pi-radius-work",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Radius Google Workspace and image generation extensions for Pi",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/earendil-works/pi-extensions.git",
|
|
11
|
+
"directory": "packages/pi-radius-work"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"extensions/radius-google-workspaces.ts",
|
|
16
|
+
"extensions/radius-image-generation.ts"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@earendil-works/pi-ai": "*",
|
|
23
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
24
|
+
"@earendil-works/pi-tui": "*"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./extensions/radius-google-workspaces.ts",
|
|
29
|
+
"./extensions/radius-image-generation.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|