@aexol/opencode-wizard 0.1.0 → 0.1.2
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 +25 -6
- package/package.json +17 -6
- package/dist/server.d.ts +0 -130
- package/dist/server.js +0 -1628
- package/dist/smoke-published-skills.d.ts +0 -1
- package/dist/smoke-published-skills.js +0 -79
package/dist/server.js
DELETED
|
@@ -1,1628 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
|
-
import { URL, fileURLToPath } from 'node:url';
|
|
8
|
-
import { tool } from '@opencode-ai/plugin';
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
|
|
11
|
-
const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
|
|
12
|
-
const PLUGIN_ID = 'opencode-wizard-skills';
|
|
13
|
-
const CACHE_TTL_MS = 30_000;
|
|
14
|
-
const ROOT_SKILL_SEED_PATH = '.opencode/skills';
|
|
15
|
-
const AUTH_STATE_PATH = 'plugin/opencode-wizard-skills/.generated/auth-state.json';
|
|
16
|
-
const LOCAL_DEV_BACKEND_ORIGIN = 'http://localhost:8080';
|
|
17
|
-
const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
|
|
18
|
-
const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
|
|
19
|
-
const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
|
|
20
|
-
const OIDC_CALLBACK_ORIGIN = 'http://localhost:24953';
|
|
21
|
-
const OIDC_CALLBACK_PATH = '/oauth/callback';
|
|
22
|
-
const OIDC_CALLBACK_URL = `${OIDC_CALLBACK_ORIGIN}${OIDC_CALLBACK_PATH}`;
|
|
23
|
-
const OIDC_SCOPES = ['openid', 'profile', 'email'];
|
|
24
|
-
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
25
|
-
const PRESENCE_EVENT_TIMEOUT_MS = 3_000;
|
|
26
|
-
const PRESENCE_EVENT_MAX_ATTEMPTS = 2;
|
|
27
|
-
const PRESENCE_EVENT_RETRY_DELAY_MS = 250;
|
|
28
|
-
const PRESENCE_SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
29
|
-
const PRESENCE_SIGNAL_EXIT_CODES = {
|
|
30
|
-
SIGINT: 130,
|
|
31
|
-
SIGTERM: 143,
|
|
32
|
-
SIGHUP: 129,
|
|
33
|
-
};
|
|
34
|
-
const createIdleLoginBootstrapSnapshot = () => ({
|
|
35
|
-
status: 'idle',
|
|
36
|
-
trigger: null,
|
|
37
|
-
startedAt: null,
|
|
38
|
-
expiresAt: null,
|
|
39
|
-
browserUrl: null,
|
|
40
|
-
browserOpenError: null,
|
|
41
|
-
email: null,
|
|
42
|
-
message: null,
|
|
43
|
-
});
|
|
44
|
-
const AVAILABLE_PUBLISHED_SKILL_TOOLS = [
|
|
45
|
-
'opencode_wizard_published_skills_fetch',
|
|
46
|
-
];
|
|
47
|
-
const PUBLISHED_SKILLS_CATALOG_QUERY = `
|
|
48
|
-
query PluginPublishedSkills($input: PublishedSkillsDeliveryInput!) {
|
|
49
|
-
pluginPublishedSkills(input: $input) {
|
|
50
|
-
workspace {
|
|
51
|
-
id
|
|
52
|
-
slug
|
|
53
|
-
name
|
|
54
|
-
repositoryUrl
|
|
55
|
-
defaultBranch
|
|
56
|
-
status
|
|
57
|
-
}
|
|
58
|
-
directoryPath
|
|
59
|
-
skills {
|
|
60
|
-
assignmentSource
|
|
61
|
-
assignmentType
|
|
62
|
-
scopePath
|
|
63
|
-
includeChildren
|
|
64
|
-
skill {
|
|
65
|
-
id
|
|
66
|
-
slug
|
|
67
|
-
name
|
|
68
|
-
summary
|
|
69
|
-
status
|
|
70
|
-
tags {
|
|
71
|
-
id
|
|
72
|
-
slug
|
|
73
|
-
label
|
|
74
|
-
description
|
|
75
|
-
facet {
|
|
76
|
-
id
|
|
77
|
-
slug
|
|
78
|
-
label
|
|
79
|
-
description
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
skillVersion {
|
|
84
|
-
id
|
|
85
|
-
version
|
|
86
|
-
title
|
|
87
|
-
summary
|
|
88
|
-
status
|
|
89
|
-
}
|
|
90
|
-
publishedArtifact {
|
|
91
|
-
id
|
|
92
|
-
frontmatterName
|
|
93
|
-
frontmatterDescription
|
|
94
|
-
checksum
|
|
95
|
-
publishedAt
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
`;
|
|
101
|
-
const PUBLISHED_SKILL_DETAIL_QUERY = `
|
|
102
|
-
query PluginPublishedSkillVersionArtifact($input: PublishedSkillArtifactDetailInput!) {
|
|
103
|
-
pluginPublishedSkillVersionArtifact(input: $input) {
|
|
104
|
-
id
|
|
105
|
-
frontmatterName
|
|
106
|
-
frontmatterDescription
|
|
107
|
-
markdownBody
|
|
108
|
-
renderedContent
|
|
109
|
-
checksum
|
|
110
|
-
publishedAt
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
`;
|
|
114
|
-
const parseDotEnvValue = (value) => {
|
|
115
|
-
const trimmedValue = value.trim();
|
|
116
|
-
if ((trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) ||
|
|
117
|
-
(trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))) {
|
|
118
|
-
return trimmedValue.slice(1, -1);
|
|
119
|
-
}
|
|
120
|
-
return trimmedValue;
|
|
121
|
-
};
|
|
122
|
-
const parseDotEnv = (raw) => {
|
|
123
|
-
const values = new Map();
|
|
124
|
-
for (const line of raw.split(/\r?\n/u)) {
|
|
125
|
-
const trimmedLine = line.trim();
|
|
126
|
-
if (!trimmedLine || trimmedLine.startsWith('#'))
|
|
127
|
-
continue;
|
|
128
|
-
const separatorIndex = trimmedLine.indexOf('=');
|
|
129
|
-
if (separatorIndex <= 0)
|
|
130
|
-
continue;
|
|
131
|
-
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
132
|
-
if (!key)
|
|
133
|
-
continue;
|
|
134
|
-
const rawValue = trimmedLine.slice(separatorIndex + 1);
|
|
135
|
-
values.set(key, parseDotEnvValue(rawValue));
|
|
136
|
-
}
|
|
137
|
-
return values;
|
|
138
|
-
};
|
|
139
|
-
const findUpwardFile = async (startDirectory, fileName) => {
|
|
140
|
-
let currentDirectory = normalizeAbsolutePath(startDirectory);
|
|
141
|
-
while (true) {
|
|
142
|
-
const candidatePath = path.join(currentDirectory, fileName);
|
|
143
|
-
try {
|
|
144
|
-
await fs.access(candidatePath);
|
|
145
|
-
return candidatePath;
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
149
|
-
if (parentDirectory === currentDirectory)
|
|
150
|
-
return null;
|
|
151
|
-
currentDirectory = parentDirectory;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
const readLocalEnvValues = async (startDirectory) => {
|
|
156
|
-
const envPath = await findUpwardFile(startDirectory, '.env');
|
|
157
|
-
if (!envPath)
|
|
158
|
-
return new Map();
|
|
159
|
-
try {
|
|
160
|
-
const raw = await fs.readFile(envPath, 'utf8');
|
|
161
|
-
return parseDotEnv(raw);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return new Map();
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
const isLocalModulePathExecution = async () => {
|
|
168
|
-
const localDevSentinelPaths = [
|
|
169
|
-
path.join(PACKAGE_ROOT_PATH, 'src', 'server.ts'),
|
|
170
|
-
path.join(PACKAGE_ROOT_PATH, 'tsconfig.json'),
|
|
171
|
-
];
|
|
172
|
-
for (const sentinelPath of localDevSentinelPaths) {
|
|
173
|
-
try {
|
|
174
|
-
await fs.access(sentinelPath);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
182
|
-
};
|
|
183
|
-
const normalizeBackendOrigin = (value) => {
|
|
184
|
-
if (!value)
|
|
185
|
-
return null;
|
|
186
|
-
try {
|
|
187
|
-
const normalizedUrl = new URL(value);
|
|
188
|
-
return normalizedUrl.origin;
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const resolveBackendOrigin = async (worktree) => {
|
|
195
|
-
const envValues = await readLocalEnvValues(worktree);
|
|
196
|
-
const configuredPort = envValues.get('PORT') ?? process.env.PORT;
|
|
197
|
-
const normalizedPort = configuredPort ? Number.parseInt(configuredPort, 10) : Number.NaN;
|
|
198
|
-
if (Number.isInteger(normalizedPort) && normalizedPort >= 1 && normalizedPort <= 65535) {
|
|
199
|
-
return `http://localhost:${normalizedPort}`;
|
|
200
|
-
}
|
|
201
|
-
const configuredAppUrl = normalizeBackendOrigin(envValues.get('APP_URL')) ?? normalizeBackendOrigin(process.env.APP_URL);
|
|
202
|
-
if (configuredAppUrl)
|
|
203
|
-
return configuredAppUrl;
|
|
204
|
-
if (await isLocalModulePathExecution())
|
|
205
|
-
return LOCAL_DEV_BACKEND_ORIGIN;
|
|
206
|
-
return PUBLISHED_BACKEND_ORIGIN;
|
|
207
|
-
};
|
|
208
|
-
const toWorkspaceSlug = (value) => {
|
|
209
|
-
const normalized = value
|
|
210
|
-
.trim()
|
|
211
|
-
.toLowerCase()
|
|
212
|
-
.replace(/[^a-z0-9-]+/gu, '-')
|
|
213
|
-
.replace(/^-+|-+$/gu, '');
|
|
214
|
-
if (normalized)
|
|
215
|
-
return normalized;
|
|
216
|
-
return 'workspace';
|
|
217
|
-
};
|
|
218
|
-
const resolveFallbackWorkspaceSlug = (worktree) => {
|
|
219
|
-
const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
|
|
220
|
-
if (configuredWorkspaceSlug)
|
|
221
|
-
return toWorkspaceSlug(configuredWorkspaceSlug);
|
|
222
|
-
return toWorkspaceSlug(path.basename(path.resolve(worktree)));
|
|
223
|
-
};
|
|
224
|
-
export const resolveConfig = async (worktree) => {
|
|
225
|
-
const backendOrigin = await resolveBackendOrigin(worktree);
|
|
226
|
-
return {
|
|
227
|
-
backendOrigin,
|
|
228
|
-
graphqlUrl: `${backendOrigin}/graphql`,
|
|
229
|
-
authSessionUrl: `${backendOrigin}/api/opencode-plugin/auth/session`,
|
|
230
|
-
presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
|
|
231
|
-
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
232
|
-
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
233
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
234
|
-
authStatePath: AUTH_STATE_PATH,
|
|
235
|
-
};
|
|
236
|
-
};
|
|
237
|
-
const normalizeAbsolutePath = (value) => path.resolve(value);
|
|
238
|
-
const normalizeRepositoryPath = (worktree, directory) => {
|
|
239
|
-
const absoluteWorktree = normalizeAbsolutePath(worktree);
|
|
240
|
-
const absoluteDirectory = normalizeAbsolutePath(directory);
|
|
241
|
-
const relativePath = path.relative(absoluteWorktree, absoluteDirectory);
|
|
242
|
-
if (!relativePath || relativePath === '')
|
|
243
|
-
return '.';
|
|
244
|
-
if (relativePath.startsWith('..') || path.isAbsolute(relativePath))
|
|
245
|
-
return '.';
|
|
246
|
-
return relativePath.split(path.sep).join('/');
|
|
247
|
-
};
|
|
248
|
-
const tryExecGit = async (args) => {
|
|
249
|
-
try {
|
|
250
|
-
const { stdout } = await execFileAsync('git', args, {
|
|
251
|
-
encoding: 'utf8',
|
|
252
|
-
});
|
|
253
|
-
const normalizedOutput = stdout.trim();
|
|
254
|
-
if (normalizedOutput)
|
|
255
|
-
return normalizedOutput;
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
const resolveGitRoot = async (directory) => {
|
|
263
|
-
const gitRoot = await tryExecGit(['-C', directory, 'rev-parse', '--show-toplevel']);
|
|
264
|
-
if (!gitRoot)
|
|
265
|
-
return null;
|
|
266
|
-
return normalizeAbsolutePath(gitRoot);
|
|
267
|
-
};
|
|
268
|
-
const normalizeGitRemoteUrl = (remoteUrl) => {
|
|
269
|
-
if (!remoteUrl)
|
|
270
|
-
return null;
|
|
271
|
-
const trimmedRemoteUrl = remoteUrl.trim();
|
|
272
|
-
if (!trimmedRemoteUrl)
|
|
273
|
-
return null;
|
|
274
|
-
const scpLikeMatch = /^git@([^:]+):(.+)$/u.exec(trimmedRemoteUrl);
|
|
275
|
-
if (scpLikeMatch) {
|
|
276
|
-
return `ssh://git@${scpLikeMatch[1]}/${scpLikeMatch[2].replace(/^\/+/, '')}`;
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
const parsedUrl = new URL(trimmedRemoteUrl);
|
|
280
|
-
return parsedUrl.toString().replace(/\/+$/u, '');
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
const resolveGitRemoteOriginUrl = async (repositoryRoot) => {
|
|
287
|
-
const remoteUrl = await tryExecGit(['-C', repositoryRoot, 'remote', 'get-url', 'origin']);
|
|
288
|
-
return normalizeGitRemoteUrl(remoteUrl);
|
|
289
|
-
};
|
|
290
|
-
const resolveWorkspace = async ({ config, directory, }) => {
|
|
291
|
-
const requestedDirectory = normalizeAbsolutePath(directory);
|
|
292
|
-
const gitRoot = await resolveGitRoot(requestedDirectory);
|
|
293
|
-
const repositoryRoot = gitRoot ?? requestedDirectory;
|
|
294
|
-
const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
|
|
295
|
-
const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
|
|
296
|
-
const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
|
|
297
|
-
const workspaceIdentity = `workspaceSlug:${fallbackWorkspaceSlug}`;
|
|
298
|
-
return {
|
|
299
|
-
requestedDirectory,
|
|
300
|
-
repositoryRoot,
|
|
301
|
-
repositoryUrl,
|
|
302
|
-
fallbackWorkspaceSlug,
|
|
303
|
-
directoryPath,
|
|
304
|
-
cacheKey: JSON.stringify([workspaceIdentity, directoryPath]),
|
|
305
|
-
};
|
|
306
|
-
};
|
|
307
|
-
const toDeliveryInput = (resolution) => {
|
|
308
|
-
if (resolution.fallbackWorkspaceSlug) {
|
|
309
|
-
return {
|
|
310
|
-
workspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
311
|
-
directoryPath: resolution.directoryPath,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (resolution.repositoryUrl) {
|
|
315
|
-
return {
|
|
316
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
317
|
-
directoryPath: resolution.directoryPath,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
workspaceSlug: 'workspace',
|
|
322
|
-
directoryPath: resolution.directoryPath,
|
|
323
|
-
};
|
|
324
|
-
};
|
|
325
|
-
const formatSkillLabel = (item) => {
|
|
326
|
-
const artifactName = item.publishedArtifact.frontmatterName.trim();
|
|
327
|
-
if (artifactName.length > 0)
|
|
328
|
-
return artifactName;
|
|
329
|
-
return item.skill.name;
|
|
330
|
-
};
|
|
331
|
-
const toFrontmatterString = (value) => JSON.stringify(value);
|
|
332
|
-
const readJsonFile = async (filePath) => {
|
|
333
|
-
try {
|
|
334
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
335
|
-
const parsed = JSON.parse(raw);
|
|
336
|
-
return parsed;
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
const writeJsonFile = async (filePath, value) => {
|
|
343
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
344
|
-
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
345
|
-
};
|
|
346
|
-
const deleteFileIfExists = async (filePath) => {
|
|
347
|
-
await fs.rm(filePath, { force: true });
|
|
348
|
-
};
|
|
349
|
-
const isRecord = (value) => {
|
|
350
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
351
|
-
};
|
|
352
|
-
const isValidIsoDateString = (value) => {
|
|
353
|
-
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
354
|
-
};
|
|
355
|
-
const isAuthState = (value) => {
|
|
356
|
-
if (!isRecord(value))
|
|
357
|
-
return false;
|
|
358
|
-
return (value.pluginId === PLUGIN_ID &&
|
|
359
|
-
typeof value.sessionToken === 'string' &&
|
|
360
|
-
isValidIsoDateString(value.expiresAt) &&
|
|
361
|
-
isValidIsoDateString(value.authenticatedAt) &&
|
|
362
|
-
typeof value.userId === 'string' &&
|
|
363
|
-
typeof value.email === 'string');
|
|
364
|
-
};
|
|
365
|
-
const readAuthState = async (authStateFile) => {
|
|
366
|
-
const storedAuthState = await readJsonFile(authStateFile);
|
|
367
|
-
if (storedAuthState === null)
|
|
368
|
-
return null;
|
|
369
|
-
if (isAuthState(storedAuthState))
|
|
370
|
-
return storedAuthState;
|
|
371
|
-
await deleteFileIfExists(authStateFile);
|
|
372
|
-
return null;
|
|
373
|
-
};
|
|
374
|
-
const writeAuthState = async (authStateFile, authState) => {
|
|
375
|
-
await writeJsonFile(authStateFile, authState);
|
|
376
|
-
};
|
|
377
|
-
const toAuthState = (session) => ({
|
|
378
|
-
pluginId: PLUGIN_ID,
|
|
379
|
-
sessionToken: session.jwtToken,
|
|
380
|
-
expiresAt: session.expiresAt,
|
|
381
|
-
authenticatedAt: new Date().toISOString(),
|
|
382
|
-
userId: session.user.id,
|
|
383
|
-
email: session.user.email,
|
|
384
|
-
});
|
|
385
|
-
const resolveStoredAuthState = async (worktree, config) => {
|
|
386
|
-
const authStateFile = path.resolve(worktree, config.authStatePath);
|
|
387
|
-
const authState = await readAuthState(authStateFile);
|
|
388
|
-
if (!authState) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
if (Date.parse(authState.expiresAt) > Date.now()) {
|
|
392
|
-
return authState;
|
|
393
|
-
}
|
|
394
|
-
await deleteFileIfExists(authStateFile);
|
|
395
|
-
return null;
|
|
396
|
-
};
|
|
397
|
-
export const buildSkillMarkdown = (item) => {
|
|
398
|
-
const artifactBody = item.publishedArtifact.markdownBody.trim();
|
|
399
|
-
const fallbackBody = item.publishedArtifact.renderedContent.trim();
|
|
400
|
-
const body = artifactBody || fallbackBody;
|
|
401
|
-
if (body.startsWith('---')) {
|
|
402
|
-
return body.endsWith('\n') ? body : `${body}\n`;
|
|
403
|
-
}
|
|
404
|
-
const name = formatSkillLabel(item);
|
|
405
|
-
const description = item.publishedArtifact.frontmatterDescription.trim() || item.skill.summary?.trim() || '';
|
|
406
|
-
const frontmatter = ['---', `name: ${toFrontmatterString(name)}`, `description: ${toFrontmatterString(description)}`, '---'].join('\n');
|
|
407
|
-
if (!body) {
|
|
408
|
-
return `${frontmatter}\n`;
|
|
409
|
-
}
|
|
410
|
-
return `${frontmatter}\n\n${body.endsWith('\n') ? body : `${body}\n`}`;
|
|
411
|
-
};
|
|
412
|
-
const getSkillIdentifiers = (item) => {
|
|
413
|
-
const candidates = [item.skill.slug, item.publishedArtifact.frontmatterName, item.skill.name];
|
|
414
|
-
const seen = new Set();
|
|
415
|
-
return candidates.reduce((all, candidate) => {
|
|
416
|
-
const normalized = candidate.trim();
|
|
417
|
-
if (!normalized)
|
|
418
|
-
return all;
|
|
419
|
-
const cacheKey = normalized.toLowerCase();
|
|
420
|
-
if (seen.has(cacheKey))
|
|
421
|
-
return all;
|
|
422
|
-
seen.add(cacheKey);
|
|
423
|
-
all.push(normalized);
|
|
424
|
-
return all;
|
|
425
|
-
}, []);
|
|
426
|
-
};
|
|
427
|
-
const toPublishedSkillFacetSummary = (facet) => ({
|
|
428
|
-
slug: facet.slug,
|
|
429
|
-
label: facet.label,
|
|
430
|
-
description: facet.description ?? null,
|
|
431
|
-
});
|
|
432
|
-
const toPublishedSkillTagSummary = (tag) => ({
|
|
433
|
-
slug: tag.slug,
|
|
434
|
-
label: tag.label,
|
|
435
|
-
description: tag.description ?? null,
|
|
436
|
-
facet: tag.facet ? toPublishedSkillFacetSummary(tag.facet) : null,
|
|
437
|
-
});
|
|
438
|
-
const getPublishedSkillFacets = (items) => {
|
|
439
|
-
const facetsBySlug = new Map();
|
|
440
|
-
for (const item of items) {
|
|
441
|
-
for (const tag of item.skill.tags) {
|
|
442
|
-
if (!tag.facet)
|
|
443
|
-
continue;
|
|
444
|
-
if (facetsBySlug.has(tag.facet.slug))
|
|
445
|
-
continue;
|
|
446
|
-
facetsBySlug.set(tag.facet.slug, toPublishedSkillFacetSummary(tag.facet));
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return [...facetsBySlug.values()].sort((left, right) => left.slug.localeCompare(right.slug));
|
|
450
|
-
};
|
|
451
|
-
const toPublishedSkillSummary = (item) => ({
|
|
452
|
-
skillSlug: item.skill.slug,
|
|
453
|
-
skillName: item.skill.name,
|
|
454
|
-
artifactName: item.publishedArtifact.frontmatterName,
|
|
455
|
-
artifactDescription: item.publishedArtifact.frontmatterDescription,
|
|
456
|
-
version: item.skillVersion.version,
|
|
457
|
-
assignmentSource: item.assignmentSource,
|
|
458
|
-
assignmentType: item.assignmentType,
|
|
459
|
-
scopePath: item.scopePath,
|
|
460
|
-
includeChildren: item.includeChildren ?? null,
|
|
461
|
-
checksum: item.publishedArtifact.checksum,
|
|
462
|
-
publishedAt: item.publishedArtifact.publishedAt,
|
|
463
|
-
identifiers: getSkillIdentifiers(item),
|
|
464
|
-
tags: item.skill.tags.map(toPublishedSkillTagSummary),
|
|
465
|
-
});
|
|
466
|
-
export const toPublishedSkillDetail = (item) => ({
|
|
467
|
-
...toPublishedSkillSummary(item),
|
|
468
|
-
skillId: item.skill.id,
|
|
469
|
-
skillVersionId: item.skillVersion.id,
|
|
470
|
-
artifactId: item.publishedArtifact.id,
|
|
471
|
-
markdownDocument: buildSkillMarkdown(item),
|
|
472
|
-
markdownBody: item.publishedArtifact.markdownBody,
|
|
473
|
-
renderedContent: item.publishedArtifact.renderedContent,
|
|
474
|
-
});
|
|
475
|
-
export const toPublishedSkillCatalog = (payload) => ({
|
|
476
|
-
pluginId: PLUGIN_ID,
|
|
477
|
-
runtimeMode: 'tool_fetch_only',
|
|
478
|
-
workspace: payload.workspace,
|
|
479
|
-
directoryPath: payload.directoryPath,
|
|
480
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
481
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
482
|
-
publishedSkillCount: payload.skills.length,
|
|
483
|
-
facets: getPublishedSkillFacets(payload.skills),
|
|
484
|
-
skills: payload.skills.map(toPublishedSkillSummary),
|
|
485
|
-
});
|
|
486
|
-
const normalizeSkillIdentifier = (value) => value.trim().toLowerCase();
|
|
487
|
-
const parseSkillIdentifiers = (value) => {
|
|
488
|
-
const seen = new Set();
|
|
489
|
-
return value
|
|
490
|
-
.split(/[\n,]/)
|
|
491
|
-
.map((item) => item.trim())
|
|
492
|
-
.filter((item) => item.length > 0)
|
|
493
|
-
.filter((item) => {
|
|
494
|
-
const normalized = normalizeSkillIdentifier(item);
|
|
495
|
-
if (seen.has(normalized))
|
|
496
|
-
return false;
|
|
497
|
-
seen.add(normalized);
|
|
498
|
-
return true;
|
|
499
|
-
});
|
|
500
|
-
};
|
|
501
|
-
const mergeSkillIdentifiers = (values) => {
|
|
502
|
-
const seen = new Set();
|
|
503
|
-
return values.filter((value) => {
|
|
504
|
-
const normalized = normalizeSkillIdentifier(value);
|
|
505
|
-
if (!normalized || seen.has(normalized))
|
|
506
|
-
return false;
|
|
507
|
-
seen.add(normalized);
|
|
508
|
-
return true;
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
const parseRequestedSkillArgs = (args) => {
|
|
512
|
-
const requestedSkills = [];
|
|
513
|
-
if (typeof args.skill === 'string') {
|
|
514
|
-
const normalizedSkill = args.skill.trim();
|
|
515
|
-
if (normalizedSkill)
|
|
516
|
-
requestedSkills.push(normalizedSkill);
|
|
517
|
-
}
|
|
518
|
-
if (typeof args.skills === 'string') {
|
|
519
|
-
requestedSkills.push(...parseSkillIdentifiers(args.skills));
|
|
520
|
-
}
|
|
521
|
-
return mergeSkillIdentifiers(requestedSkills);
|
|
522
|
-
};
|
|
523
|
-
export const selectPublishedSkills = (payload, identifiers) => {
|
|
524
|
-
const itemsByIdentifier = new Map();
|
|
525
|
-
for (const item of payload.skills) {
|
|
526
|
-
for (const identifier of getSkillIdentifiers(item)) {
|
|
527
|
-
itemsByIdentifier.set(normalizeSkillIdentifier(identifier), item);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
const selectedItems = [];
|
|
531
|
-
const selectedKeys = new Set();
|
|
532
|
-
const missingIdentifiers = [];
|
|
533
|
-
for (const identifier of identifiers) {
|
|
534
|
-
const matched = itemsByIdentifier.get(normalizeSkillIdentifier(identifier));
|
|
535
|
-
if (!matched) {
|
|
536
|
-
missingIdentifiers.push(identifier);
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
if (selectedKeys.has(matched.publishedArtifact.id)) {
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
selectedKeys.add(matched.publishedArtifact.id);
|
|
543
|
-
selectedItems.push(matched);
|
|
544
|
-
}
|
|
545
|
-
return {
|
|
546
|
-
selectedItems,
|
|
547
|
-
missingIdentifiers,
|
|
548
|
-
};
|
|
549
|
-
};
|
|
550
|
-
const buildSystemNote = (result, config) => {
|
|
551
|
-
if (!result.fetchResult.ok)
|
|
552
|
-
return null;
|
|
553
|
-
const skillNames = result.fetchResult.payload.skills.map(formatSkillLabel);
|
|
554
|
-
const renderedSkillNames = skillNames.length > 0 ? skillNames.slice(0, 8).join(', ') : 'none';
|
|
555
|
-
const remainingCount = Math.max(skillNames.length - 8, 0);
|
|
556
|
-
const renderedCountSuffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
|
|
557
|
-
return [
|
|
558
|
-
`opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.`,
|
|
559
|
-
`Current directory: ${result.directoryPath}.`,
|
|
560
|
-
`Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}.`,
|
|
561
|
-
'Use opencode_wizard_published_skills_fetch for one or multiple skills.',
|
|
562
|
-
`Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`,
|
|
563
|
-
].join(' ');
|
|
564
|
-
};
|
|
565
|
-
const toWorkspaceResolutionOutput = (resolution) => ({
|
|
566
|
-
requestedDirectory: resolution.requestedDirectory,
|
|
567
|
-
repositoryRoot: resolution.repositoryRoot,
|
|
568
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
569
|
-
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
570
|
-
directoryPath: resolution.directoryPath,
|
|
571
|
-
});
|
|
572
|
-
const toWorkspaceResolutionMetadata = (resolution) => ({
|
|
573
|
-
directoryPath: resolution.directoryPath,
|
|
574
|
-
repositoryRoot: resolution.repositoryRoot,
|
|
575
|
-
repositoryUrl: resolution.repositoryUrl ?? '',
|
|
576
|
-
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? '',
|
|
577
|
-
});
|
|
578
|
-
const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
|
|
579
|
-
const authState = await resolveStoredAuthState(worktree, config);
|
|
580
|
-
const base = {
|
|
581
|
-
pluginId: PLUGIN_ID,
|
|
582
|
-
runtimeMode: 'tool_fetch_only',
|
|
583
|
-
backendOrigin: config.backendOrigin,
|
|
584
|
-
graphqlUrl: config.graphqlUrl,
|
|
585
|
-
fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
|
|
586
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
587
|
-
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
588
|
-
authStatePath: config.authStatePath,
|
|
589
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
590
|
-
authMode: publishedSkillsResult.fetchResult.authMode,
|
|
591
|
-
authState: authState === null
|
|
592
|
-
? null
|
|
593
|
-
: {
|
|
594
|
-
email: authState.email,
|
|
595
|
-
userId: authState.userId,
|
|
596
|
-
authenticatedAt: authState.authenticatedAt,
|
|
597
|
-
expiresAt: authState.expiresAt,
|
|
598
|
-
},
|
|
599
|
-
loginBootstrap: loginBootstrapSnapshot.status === 'idle'
|
|
600
|
-
? null
|
|
601
|
-
: {
|
|
602
|
-
status: loginBootstrapSnapshot.status,
|
|
603
|
-
trigger: loginBootstrapSnapshot.trigger,
|
|
604
|
-
startedAt: loginBootstrapSnapshot.startedAt,
|
|
605
|
-
expiresAt: loginBootstrapSnapshot.expiresAt,
|
|
606
|
-
browserUrl: loginBootstrapSnapshot.browserUrl,
|
|
607
|
-
browserOpenError: loginBootstrapSnapshot.browserOpenError,
|
|
608
|
-
email: loginBootstrapSnapshot.email,
|
|
609
|
-
message: loginBootstrapSnapshot.message,
|
|
610
|
-
},
|
|
611
|
-
status: publishedSkillsResult.fetchResult.status,
|
|
612
|
-
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
613
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
614
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
615
|
-
};
|
|
616
|
-
if (!publishedSkillsResult.fetchResult.ok) {
|
|
617
|
-
return JSON.stringify({
|
|
618
|
-
...base,
|
|
619
|
-
message: publishedSkillsResult.fetchResult.message,
|
|
620
|
-
}, null, 2);
|
|
621
|
-
}
|
|
622
|
-
return JSON.stringify({
|
|
623
|
-
...base,
|
|
624
|
-
...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload),
|
|
625
|
-
}, null, 2);
|
|
626
|
-
};
|
|
627
|
-
const isUnauthorizedGraphQlMessage = (message) => {
|
|
628
|
-
const normalizedMessage = message.toLowerCase();
|
|
629
|
-
return normalizedMessage.includes('not authorized') || normalizedMessage.includes('unauthorized');
|
|
630
|
-
};
|
|
631
|
-
const createRandomBase64Url = (bytes) => {
|
|
632
|
-
return crypto.randomBytes(bytes).toString('base64url');
|
|
633
|
-
};
|
|
634
|
-
const createCodeChallenge = (codeVerifier) => {
|
|
635
|
-
return crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
636
|
-
};
|
|
637
|
-
const getMessageFromUnknownPayload = (value) => {
|
|
638
|
-
if (!value || typeof value !== 'object')
|
|
639
|
-
return null;
|
|
640
|
-
const candidate = 'message' in value ? value.message : null;
|
|
641
|
-
return typeof candidate === 'string' ? candidate : null;
|
|
642
|
-
};
|
|
643
|
-
const wait = async (milliseconds) => {
|
|
644
|
-
await new Promise((resolve) => {
|
|
645
|
-
setTimeout(resolve, milliseconds);
|
|
646
|
-
});
|
|
647
|
-
};
|
|
648
|
-
const shouldRetryPresenceEvent = (status) => {
|
|
649
|
-
return status === 408 || status === 429 || status >= 500;
|
|
650
|
-
};
|
|
651
|
-
const fetchOidcDiscoveryDocument = async (signal) => {
|
|
652
|
-
const discoveryUrl = `${OIDC_ISSUER.replace(/\/+$/, '')}/.well-known/openid-configuration`;
|
|
653
|
-
const response = await fetch(discoveryUrl, {
|
|
654
|
-
method: 'GET',
|
|
655
|
-
signal,
|
|
656
|
-
});
|
|
657
|
-
if (!response.ok) {
|
|
658
|
-
throw new Error(`OIDC discovery failed with HTTP ${response.status}.`);
|
|
659
|
-
}
|
|
660
|
-
return (await response.json());
|
|
661
|
-
};
|
|
662
|
-
const sendHtmlResponse = (response, statusCode, title, message) => {
|
|
663
|
-
response.writeHead(statusCode, {
|
|
664
|
-
'content-type': 'text/html; charset=utf-8',
|
|
665
|
-
'cache-control': 'no-store',
|
|
666
|
-
});
|
|
667
|
-
response.end(`<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h1>${title}</h1><p>${message}</p><p>You can close this window and return to OpenCode.</p></body></html>`);
|
|
668
|
-
};
|
|
669
|
-
const startLocalCallbackServer = async ({ expectedState, signal, }) => {
|
|
670
|
-
let settled = false;
|
|
671
|
-
let resolvePayload = () => undefined;
|
|
672
|
-
let rejectPayload = () => undefined;
|
|
673
|
-
const callbackPromise = new Promise((resolve, reject) => {
|
|
674
|
-
resolvePayload = resolve;
|
|
675
|
-
rejectPayload = reject;
|
|
676
|
-
});
|
|
677
|
-
const finalize = (payload) => {
|
|
678
|
-
if (settled)
|
|
679
|
-
return;
|
|
680
|
-
settled = true;
|
|
681
|
-
resolvePayload(payload);
|
|
682
|
-
};
|
|
683
|
-
const fail = (reason) => {
|
|
684
|
-
if (settled)
|
|
685
|
-
return;
|
|
686
|
-
settled = true;
|
|
687
|
-
rejectPayload(reason);
|
|
688
|
-
};
|
|
689
|
-
const server = http.createServer((request, response) => {
|
|
690
|
-
const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
|
|
691
|
-
if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
|
|
692
|
-
sendHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const error = requestUrl.searchParams.get('error');
|
|
696
|
-
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
697
|
-
if (error) {
|
|
698
|
-
const message = errorDescription ?? error;
|
|
699
|
-
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
|
|
700
|
-
finalize({
|
|
701
|
-
status: 'error',
|
|
702
|
-
message,
|
|
703
|
-
});
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
const state = requestUrl.searchParams.get('state');
|
|
707
|
-
const code = requestUrl.searchParams.get('code');
|
|
708
|
-
if (!state || state !== expectedState) {
|
|
709
|
-
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
|
|
710
|
-
finalize({
|
|
711
|
-
status: 'error',
|
|
712
|
-
message: 'OAuth state did not match the login request.',
|
|
713
|
-
});
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (!code) {
|
|
717
|
-
sendHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
|
|
718
|
-
finalize({
|
|
719
|
-
status: 'error',
|
|
720
|
-
message: 'OAuth callback did not include an authorization code.',
|
|
721
|
-
});
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
sendHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
|
|
725
|
-
finalize({
|
|
726
|
-
status: 'success',
|
|
727
|
-
code,
|
|
728
|
-
state,
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
server.on('error', (error) => {
|
|
732
|
-
fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
|
|
733
|
-
});
|
|
734
|
-
const close = async () => {
|
|
735
|
-
await new Promise((resolve, reject) => {
|
|
736
|
-
server.close((error) => {
|
|
737
|
-
if (error) {
|
|
738
|
-
reject(error);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
resolve();
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
};
|
|
745
|
-
await new Promise((resolve, reject) => {
|
|
746
|
-
server.listen(24953, 'localhost', () => resolve());
|
|
747
|
-
server.once('error', reject);
|
|
748
|
-
});
|
|
749
|
-
signal.addEventListener('abort', () => {
|
|
750
|
-
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
751
|
-
void close().catch(() => undefined);
|
|
752
|
-
}, { once: true });
|
|
753
|
-
return {
|
|
754
|
-
callbackPromise,
|
|
755
|
-
close,
|
|
756
|
-
};
|
|
757
|
-
};
|
|
758
|
-
const fetchPublishedSkillsGraphQl = async ({ worktree, config, query, variables, signal, onAuthStateChanged, }) => {
|
|
759
|
-
const authState = await resolveStoredAuthState(worktree, config);
|
|
760
|
-
const fetchedAt = new Date().toISOString();
|
|
761
|
-
if (!authState) {
|
|
762
|
-
return {
|
|
763
|
-
ok: false,
|
|
764
|
-
result: {
|
|
765
|
-
ok: false,
|
|
766
|
-
status: 'missing_auth',
|
|
767
|
-
authMode: 'missing',
|
|
768
|
-
message: 'No plugin session is stored. Interactive opencode_wizard_published_skills_fetch can bootstrap browser login automatically when needed.',
|
|
769
|
-
fetchedAt,
|
|
770
|
-
source: 'network',
|
|
771
|
-
},
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
let response;
|
|
775
|
-
try {
|
|
776
|
-
response = await fetch(config.graphqlUrl, {
|
|
777
|
-
method: 'POST',
|
|
778
|
-
headers: {
|
|
779
|
-
'content-type': 'application/json',
|
|
780
|
-
authorization: `Bearer ${authState.sessionToken}`,
|
|
781
|
-
},
|
|
782
|
-
body: JSON.stringify({
|
|
783
|
-
query,
|
|
784
|
-
variables,
|
|
785
|
-
}),
|
|
786
|
-
signal,
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
catch (error) {
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
result: {
|
|
793
|
-
ok: false,
|
|
794
|
-
status: 'request_failed',
|
|
795
|
-
authMode: 'session',
|
|
796
|
-
message: error instanceof Error ? error.message : 'Unknown fetch error',
|
|
797
|
-
fetchedAt,
|
|
798
|
-
source: 'network',
|
|
799
|
-
},
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
if (response.status === 401 || response.status === 403) {
|
|
803
|
-
await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
|
|
804
|
-
onAuthStateChanged?.();
|
|
805
|
-
return {
|
|
806
|
-
ok: false,
|
|
807
|
-
result: {
|
|
808
|
-
ok: false,
|
|
809
|
-
status: 'missing_auth',
|
|
810
|
-
authMode: 'session',
|
|
811
|
-
message: 'Stored plugin session was rejected by the backend. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
812
|
-
fetchedAt,
|
|
813
|
-
source: 'network',
|
|
814
|
-
},
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
if (!response.ok) {
|
|
818
|
-
return {
|
|
819
|
-
ok: false,
|
|
820
|
-
result: {
|
|
821
|
-
ok: false,
|
|
822
|
-
status: 'request_failed',
|
|
823
|
-
authMode: 'session',
|
|
824
|
-
message: `GraphQL request failed with HTTP ${response.status}.`,
|
|
825
|
-
fetchedAt,
|
|
826
|
-
source: 'network',
|
|
827
|
-
},
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
let body;
|
|
831
|
-
try {
|
|
832
|
-
body = (await response.json());
|
|
833
|
-
}
|
|
834
|
-
catch (error) {
|
|
835
|
-
return {
|
|
836
|
-
ok: false,
|
|
837
|
-
result: {
|
|
838
|
-
ok: false,
|
|
839
|
-
status: 'request_failed',
|
|
840
|
-
authMode: 'session',
|
|
841
|
-
message: `GraphQL response was not valid JSON: ${error instanceof Error ? error.message : 'Unknown parse error'}`,
|
|
842
|
-
fetchedAt,
|
|
843
|
-
source: 'network',
|
|
844
|
-
},
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
if (body.errors?.length) {
|
|
848
|
-
const message = body.errors.map((error) => error.message).join('; ');
|
|
849
|
-
if (body.errors.some((error) => isUnauthorizedGraphQlMessage(error.message))) {
|
|
850
|
-
await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
|
|
851
|
-
onAuthStateChanged?.();
|
|
852
|
-
return {
|
|
853
|
-
ok: false,
|
|
854
|
-
result: {
|
|
855
|
-
ok: false,
|
|
856
|
-
status: 'missing_auth',
|
|
857
|
-
authMode: 'session',
|
|
858
|
-
message: 'Stored plugin session is no longer valid. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
859
|
-
fetchedAt,
|
|
860
|
-
source: 'network',
|
|
861
|
-
},
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
return {
|
|
865
|
-
ok: false,
|
|
866
|
-
result: {
|
|
867
|
-
ok: false,
|
|
868
|
-
status: 'request_failed',
|
|
869
|
-
authMode: 'session',
|
|
870
|
-
message,
|
|
871
|
-
fetchedAt,
|
|
872
|
-
source: 'network',
|
|
873
|
-
},
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
if (!body.data) {
|
|
877
|
-
return {
|
|
878
|
-
ok: false,
|
|
879
|
-
result: {
|
|
880
|
-
ok: false,
|
|
881
|
-
status: 'request_failed',
|
|
882
|
-
authMode: 'session',
|
|
883
|
-
message: 'GraphQL response did not include data.',
|
|
884
|
-
fetchedAt,
|
|
885
|
-
source: 'network',
|
|
886
|
-
},
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
ok: true,
|
|
891
|
-
data: body.data,
|
|
892
|
-
fetchedAt,
|
|
893
|
-
};
|
|
894
|
-
};
|
|
895
|
-
const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal, onAuthStateChanged) => {
|
|
896
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
897
|
-
worktree,
|
|
898
|
-
config,
|
|
899
|
-
query: PUBLISHED_SKILLS_CATALOG_QUERY,
|
|
900
|
-
variables: {
|
|
901
|
-
input: toDeliveryInput(resolution),
|
|
902
|
-
},
|
|
903
|
-
signal,
|
|
904
|
-
onAuthStateChanged,
|
|
905
|
-
});
|
|
906
|
-
if (!response.ok) {
|
|
907
|
-
return response.result;
|
|
908
|
-
}
|
|
909
|
-
const payload = response.data.pluginPublishedSkills;
|
|
910
|
-
if (!payload) {
|
|
911
|
-
return {
|
|
912
|
-
ok: false,
|
|
913
|
-
status: 'request_failed',
|
|
914
|
-
authMode: 'session',
|
|
915
|
-
message: 'GraphQL response did not include pluginPublishedSkills.',
|
|
916
|
-
fetchedAt: response.fetchedAt,
|
|
917
|
-
source: 'network',
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
return {
|
|
921
|
-
ok: true,
|
|
922
|
-
status: 'ready',
|
|
923
|
-
authMode: 'session',
|
|
924
|
-
payload,
|
|
925
|
-
fetchedAt: response.fetchedAt,
|
|
926
|
-
source: 'network',
|
|
927
|
-
};
|
|
928
|
-
};
|
|
929
|
-
const fetchPublishedSkillDetail = async ({ worktree, config, resolution, skillVersionId, signal, onAuthStateChanged, }) => {
|
|
930
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
931
|
-
worktree,
|
|
932
|
-
config,
|
|
933
|
-
query: PUBLISHED_SKILL_DETAIL_QUERY,
|
|
934
|
-
variables: {
|
|
935
|
-
input: {
|
|
936
|
-
...toDeliveryInput(resolution),
|
|
937
|
-
skillVersionId,
|
|
938
|
-
},
|
|
939
|
-
},
|
|
940
|
-
signal,
|
|
941
|
-
onAuthStateChanged,
|
|
942
|
-
});
|
|
943
|
-
if (!response.ok) {
|
|
944
|
-
return response;
|
|
945
|
-
}
|
|
946
|
-
const artifact = response.data.pluginPublishedSkillVersionArtifact;
|
|
947
|
-
if (!artifact) {
|
|
948
|
-
return {
|
|
949
|
-
ok: false,
|
|
950
|
-
result: {
|
|
951
|
-
ok: false,
|
|
952
|
-
status: 'not_found',
|
|
953
|
-
authMode: 'session',
|
|
954
|
-
message: 'Published skill detail is not effective for the current scope.',
|
|
955
|
-
fetchedAt: response.fetchedAt,
|
|
956
|
-
source: 'network',
|
|
957
|
-
},
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
return {
|
|
961
|
-
ok: true,
|
|
962
|
-
artifact,
|
|
963
|
-
};
|
|
964
|
-
};
|
|
965
|
-
const toFetchFailureOutput = async ({ worktree, config, publishedSkillsResult, loginBootstrapSnapshot, }) => ({
|
|
966
|
-
output: await formatStatusOutput(worktree, config, publishedSkillsResult, loginBootstrapSnapshot),
|
|
967
|
-
metadata: {
|
|
968
|
-
status: publishedSkillsResult.fetchResult.status,
|
|
969
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
970
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
971
|
-
},
|
|
972
|
-
});
|
|
973
|
-
const startLoginFlow = async (signal) => {
|
|
974
|
-
const discovery = await fetchOidcDiscoveryDocument(signal);
|
|
975
|
-
const codeVerifier = createRandomBase64Url(64);
|
|
976
|
-
const expectedState = createRandomBase64Url(32);
|
|
977
|
-
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
978
|
-
const expiresAt = new Date(Date.now() + LOGIN_TIMEOUT_MS).toISOString();
|
|
979
|
-
const { callbackPromise, close } = await startLocalCallbackServer({
|
|
980
|
-
expectedState,
|
|
981
|
-
signal,
|
|
982
|
-
});
|
|
983
|
-
const browserUrl = new URL(discovery.authorization_endpoint);
|
|
984
|
-
browserUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
985
|
-
browserUrl.searchParams.set('response_type', 'code');
|
|
986
|
-
browserUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
987
|
-
browserUrl.searchParams.set('response_mode', 'query');
|
|
988
|
-
browserUrl.searchParams.set('scope', OIDC_SCOPES.join(' '));
|
|
989
|
-
browserUrl.searchParams.set('code_challenge', codeChallenge);
|
|
990
|
-
browserUrl.searchParams.set('code_challenge_method', 'S256');
|
|
991
|
-
browserUrl.searchParams.set('state', expectedState);
|
|
992
|
-
return {
|
|
993
|
-
browserUrl: browserUrl.toString(),
|
|
994
|
-
expiresAt,
|
|
995
|
-
codeVerifier,
|
|
996
|
-
expectedState,
|
|
997
|
-
callbackPromise,
|
|
998
|
-
closeCallbackServer: close,
|
|
999
|
-
};
|
|
1000
|
-
};
|
|
1001
|
-
const createPluginSession = async ({ code, codeVerifier, redirectUri, config, signal, }) => {
|
|
1002
|
-
const response = await fetch(config.authSessionUrl, {
|
|
1003
|
-
method: 'POST',
|
|
1004
|
-
headers: {
|
|
1005
|
-
'content-type': 'application/json',
|
|
1006
|
-
},
|
|
1007
|
-
body: JSON.stringify({
|
|
1008
|
-
code,
|
|
1009
|
-
codeVerifier,
|
|
1010
|
-
redirectUri,
|
|
1011
|
-
}),
|
|
1012
|
-
signal,
|
|
1013
|
-
});
|
|
1014
|
-
const payload = (await response.json().catch(() => null));
|
|
1015
|
-
if (!response.ok) {
|
|
1016
|
-
throw new Error(getMessageFromUnknownPayload(payload) ?? `Plugin session exchange failed with HTTP ${response.status}.`);
|
|
1017
|
-
}
|
|
1018
|
-
if (!payload || !('success' in payload) || payload.success !== true) {
|
|
1019
|
-
throw new Error('Plugin session exchange returned an unexpected payload.');
|
|
1020
|
-
}
|
|
1021
|
-
return payload.session;
|
|
1022
|
-
};
|
|
1023
|
-
const emitPresenceEvent = async ({ config, authState, event, workspacePath, }) => {
|
|
1024
|
-
for (let attempt = 1; attempt <= PRESENCE_EVENT_MAX_ATTEMPTS; attempt += 1) {
|
|
1025
|
-
try {
|
|
1026
|
-
const response = await fetch(config.presenceUrl, {
|
|
1027
|
-
method: 'POST',
|
|
1028
|
-
headers: {
|
|
1029
|
-
'content-type': 'application/json',
|
|
1030
|
-
authorization: `Bearer ${authState.sessionToken}`,
|
|
1031
|
-
},
|
|
1032
|
-
body: JSON.stringify({
|
|
1033
|
-
event,
|
|
1034
|
-
occurredAt: new Date().toISOString(),
|
|
1035
|
-
workspacePath,
|
|
1036
|
-
}),
|
|
1037
|
-
keepalive: event === 'STOP',
|
|
1038
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS),
|
|
1039
|
-
});
|
|
1040
|
-
if (response.ok)
|
|
1041
|
-
return;
|
|
1042
|
-
if (!shouldRetryPresenceEvent(response.status) || attempt === PRESENCE_EVENT_MAX_ATTEMPTS)
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
catch {
|
|
1046
|
-
if (attempt === PRESENCE_EVENT_MAX_ATTEMPTS)
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
await wait(PRESENCE_EVENT_RETRY_DELAY_MS * attempt);
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
const emitPluginActionEvent = async ({ config, authState, event, workspacePath, directoryPath, }) => {
|
|
1053
|
-
if (!authState)
|
|
1054
|
-
return;
|
|
1055
|
-
try {
|
|
1056
|
-
await fetch(config.actionsUrl, {
|
|
1057
|
-
method: 'POST',
|
|
1058
|
-
headers: {
|
|
1059
|
-
'content-type': 'application/json',
|
|
1060
|
-
authorization: `Bearer ${authState.sessionToken}`,
|
|
1061
|
-
},
|
|
1062
|
-
body: JSON.stringify({
|
|
1063
|
-
event,
|
|
1064
|
-
occurredAt: new Date().toISOString(),
|
|
1065
|
-
workspacePath,
|
|
1066
|
-
directoryPath,
|
|
1067
|
-
}),
|
|
1068
|
-
keepalive: event === 'STOP',
|
|
1069
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS),
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
catch {
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
const openBrowser = async (url) => {
|
|
1077
|
-
try {
|
|
1078
|
-
if (process.platform === 'darwin') {
|
|
1079
|
-
await execFileAsync('open', [url]);
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
if (process.platform === 'win32') {
|
|
1083
|
-
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
await execFileAsync('xdg-open', [url]);
|
|
1087
|
-
return null;
|
|
1088
|
-
}
|
|
1089
|
-
catch (error) {
|
|
1090
|
-
return error instanceof Error ? error.message : 'Failed to open browser automatically';
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
const normalizeDirectoryArg = (contextDirectory, directory) => {
|
|
1094
|
-
return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
|
|
1095
|
-
};
|
|
1096
|
-
const getDetailCacheKey = (workspaceResolution, skillVersionId) => {
|
|
1097
|
-
return JSON.stringify([workspaceResolution.cacheKey, skillVersionId]);
|
|
1098
|
-
};
|
|
1099
|
-
const OpencodeWizardSkillsPlugin = async (input) => {
|
|
1100
|
-
const config = await resolveConfig(input.worktree);
|
|
1101
|
-
const workspacePath = normalizeAbsolutePath(input.worktree);
|
|
1102
|
-
const cache = new Map();
|
|
1103
|
-
const catalogInflight = new Map();
|
|
1104
|
-
const detailCache = new Map();
|
|
1105
|
-
const detailInflight = new Map();
|
|
1106
|
-
const authStateFile = path.resolve(input.worktree, config.authStatePath);
|
|
1107
|
-
const initialAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1108
|
-
const loginBootstrap = {
|
|
1109
|
-
promise: null,
|
|
1110
|
-
snapshot: createIdleLoginBootstrapSnapshot(),
|
|
1111
|
-
};
|
|
1112
|
-
let lastAuthenticatedAuthState = initialAuthState;
|
|
1113
|
-
let didEmitStart = false;
|
|
1114
|
-
let didScheduleStop = false;
|
|
1115
|
-
let presenceStartPromise = null;
|
|
1116
|
-
let presenceStopPromise = null;
|
|
1117
|
-
let lastInteractiveDirectoryPath = null;
|
|
1118
|
-
const resolveActionAuthState = async () => {
|
|
1119
|
-
const storedAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1120
|
-
if (storedAuthState)
|
|
1121
|
-
return storedAuthState;
|
|
1122
|
-
return lastAuthenticatedAuthState;
|
|
1123
|
-
};
|
|
1124
|
-
const emitActionEventForCurrentSession = async ({ event, authState, directoryPath, }) => {
|
|
1125
|
-
await emitPluginActionEvent({
|
|
1126
|
-
config,
|
|
1127
|
-
authState: authState ?? (await resolveActionAuthState()),
|
|
1128
|
-
event,
|
|
1129
|
-
workspacePath,
|
|
1130
|
-
directoryPath,
|
|
1131
|
-
});
|
|
1132
|
-
};
|
|
1133
|
-
const schedulePresenceStart = (authState) => {
|
|
1134
|
-
lastAuthenticatedAuthState = authState;
|
|
1135
|
-
if (didEmitStart) {
|
|
1136
|
-
return presenceStartPromise ?? Promise.resolve();
|
|
1137
|
-
}
|
|
1138
|
-
didEmitStart = true;
|
|
1139
|
-
presenceStartPromise = Promise.all([
|
|
1140
|
-
emitPresenceEvent({
|
|
1141
|
-
config,
|
|
1142
|
-
authState,
|
|
1143
|
-
event: 'START',
|
|
1144
|
-
workspacePath,
|
|
1145
|
-
}),
|
|
1146
|
-
emitActionEventForCurrentSession({
|
|
1147
|
-
event: 'START',
|
|
1148
|
-
authState,
|
|
1149
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined,
|
|
1150
|
-
}),
|
|
1151
|
-
]).then(() => undefined);
|
|
1152
|
-
return presenceStartPromise;
|
|
1153
|
-
};
|
|
1154
|
-
const schedulePresenceStop = () => {
|
|
1155
|
-
if (didScheduleStop) {
|
|
1156
|
-
return presenceStopPromise ?? Promise.resolve();
|
|
1157
|
-
}
|
|
1158
|
-
didScheduleStop = true;
|
|
1159
|
-
if (!didEmitStart || !lastAuthenticatedAuthState) {
|
|
1160
|
-
presenceStopPromise = Promise.resolve();
|
|
1161
|
-
return presenceStopPromise;
|
|
1162
|
-
}
|
|
1163
|
-
presenceStopPromise = (async () => {
|
|
1164
|
-
await presenceStartPromise?.catch(() => undefined);
|
|
1165
|
-
await Promise.all([
|
|
1166
|
-
emitPresenceEvent({
|
|
1167
|
-
config,
|
|
1168
|
-
authState: lastAuthenticatedAuthState,
|
|
1169
|
-
event: 'STOP',
|
|
1170
|
-
workspacePath,
|
|
1171
|
-
}),
|
|
1172
|
-
emitActionEventForCurrentSession({
|
|
1173
|
-
event: 'STOP',
|
|
1174
|
-
authState: lastAuthenticatedAuthState,
|
|
1175
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined,
|
|
1176
|
-
}),
|
|
1177
|
-
]);
|
|
1178
|
-
})();
|
|
1179
|
-
return presenceStopPromise;
|
|
1180
|
-
};
|
|
1181
|
-
const scheduleInteractivePresenceStart = async () => {
|
|
1182
|
-
const authState = await resolveStoredAuthState(input.worktree, config);
|
|
1183
|
-
if (!authState)
|
|
1184
|
-
return;
|
|
1185
|
-
await schedulePresenceStart(authState);
|
|
1186
|
-
};
|
|
1187
|
-
process.once('beforeExit', () => {
|
|
1188
|
-
void schedulePresenceStop();
|
|
1189
|
-
});
|
|
1190
|
-
for (const shutdownSignal of PRESENCE_SHUTDOWN_SIGNALS) {
|
|
1191
|
-
try {
|
|
1192
|
-
process.once(shutdownSignal, () => {
|
|
1193
|
-
void schedulePresenceStop().finally(() => {
|
|
1194
|
-
process.exit(PRESENCE_SIGNAL_EXIT_CODES[shutdownSignal]);
|
|
1195
|
-
});
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
catch {
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
const clearPublishedSkillState = () => {
|
|
1203
|
-
cache.clear();
|
|
1204
|
-
catalogInflight.clear();
|
|
1205
|
-
detailCache.clear();
|
|
1206
|
-
detailInflight.clear();
|
|
1207
|
-
};
|
|
1208
|
-
const persistAuthState = async (session) => {
|
|
1209
|
-
const authState = toAuthState(session);
|
|
1210
|
-
await writeAuthState(authStateFile, authState);
|
|
1211
|
-
clearPublishedSkillState();
|
|
1212
|
-
return authState;
|
|
1213
|
-
};
|
|
1214
|
-
const startLoginCompletion = (trigger) => {
|
|
1215
|
-
if (loginBootstrap.promise) {
|
|
1216
|
-
return loginBootstrap.promise;
|
|
1217
|
-
}
|
|
1218
|
-
const startedAt = new Date().toISOString();
|
|
1219
|
-
loginBootstrap.snapshot = {
|
|
1220
|
-
status: 'starting',
|
|
1221
|
-
trigger,
|
|
1222
|
-
startedAt,
|
|
1223
|
-
expiresAt: null,
|
|
1224
|
-
browserUrl: null,
|
|
1225
|
-
browserOpenError: null,
|
|
1226
|
-
email: null,
|
|
1227
|
-
message: null,
|
|
1228
|
-
};
|
|
1229
|
-
const loginPromise = (async () => {
|
|
1230
|
-
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1231
|
-
const loginStart = await startLoginFlow(loginSignal);
|
|
1232
|
-
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1233
|
-
loginBootstrap.snapshot = {
|
|
1234
|
-
status: 'pending',
|
|
1235
|
-
trigger,
|
|
1236
|
-
startedAt,
|
|
1237
|
-
expiresAt: loginStart.expiresAt,
|
|
1238
|
-
browserUrl: loginStart.browserUrl,
|
|
1239
|
-
browserOpenError,
|
|
1240
|
-
email: null,
|
|
1241
|
-
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : 'Browser login started for published skill fetch.',
|
|
1242
|
-
};
|
|
1243
|
-
try {
|
|
1244
|
-
const callbackPayload = await loginStart.callbackPromise;
|
|
1245
|
-
if (callbackPayload.status === 'error') {
|
|
1246
|
-
throw new Error(callbackPayload.message);
|
|
1247
|
-
}
|
|
1248
|
-
if (callbackPayload.state !== loginStart.expectedState) {
|
|
1249
|
-
throw new Error('OAuth callback state did not match the original login request.');
|
|
1250
|
-
}
|
|
1251
|
-
loginBootstrap.snapshot = {
|
|
1252
|
-
status: 'pending',
|
|
1253
|
-
trigger,
|
|
1254
|
-
startedAt,
|
|
1255
|
-
expiresAt: loginStart.expiresAt,
|
|
1256
|
-
browserUrl: loginStart.browserUrl,
|
|
1257
|
-
browserOpenError,
|
|
1258
|
-
email: null,
|
|
1259
|
-
message: 'OAuth callback received. Finalizing backend session exchange.',
|
|
1260
|
-
};
|
|
1261
|
-
const pluginSession = await createPluginSession({
|
|
1262
|
-
code: callbackPayload.code,
|
|
1263
|
-
codeVerifier: loginStart.codeVerifier,
|
|
1264
|
-
redirectUri: OIDC_CALLBACK_URL,
|
|
1265
|
-
config,
|
|
1266
|
-
signal: loginSignal,
|
|
1267
|
-
});
|
|
1268
|
-
const authState = await persistAuthState(pluginSession);
|
|
1269
|
-
await emitActionEventForCurrentSession({
|
|
1270
|
-
event: 'LOGIN_SUCCESS',
|
|
1271
|
-
authState,
|
|
1272
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined,
|
|
1273
|
-
});
|
|
1274
|
-
loginBootstrap.snapshot = {
|
|
1275
|
-
status: 'authenticated',
|
|
1276
|
-
trigger,
|
|
1277
|
-
startedAt,
|
|
1278
|
-
expiresAt: authState.expiresAt,
|
|
1279
|
-
browserUrl: loginStart.browserUrl,
|
|
1280
|
-
browserOpenError,
|
|
1281
|
-
email: authState.email,
|
|
1282
|
-
message: 'Browser login completed successfully for published skill fetch.',
|
|
1283
|
-
};
|
|
1284
|
-
return authState;
|
|
1285
|
-
}
|
|
1286
|
-
catch (error) {
|
|
1287
|
-
await emitActionEventForCurrentSession({
|
|
1288
|
-
event: 'LOGIN_FAILED',
|
|
1289
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined,
|
|
1290
|
-
});
|
|
1291
|
-
loginBootstrap.snapshot = {
|
|
1292
|
-
status: 'failed',
|
|
1293
|
-
trigger,
|
|
1294
|
-
startedAt,
|
|
1295
|
-
expiresAt: loginBootstrap.snapshot.expiresAt,
|
|
1296
|
-
browserUrl: loginBootstrap.snapshot.browserUrl,
|
|
1297
|
-
browserOpenError: loginBootstrap.snapshot.browserOpenError,
|
|
1298
|
-
email: null,
|
|
1299
|
-
message: error instanceof Error ? error.message : 'Browser login failed.',
|
|
1300
|
-
};
|
|
1301
|
-
throw error;
|
|
1302
|
-
}
|
|
1303
|
-
finally {
|
|
1304
|
-
await loginStart.closeCallbackServer().catch(() => undefined);
|
|
1305
|
-
loginBootstrap.promise = null;
|
|
1306
|
-
}
|
|
1307
|
-
})();
|
|
1308
|
-
loginBootstrap.promise = loginPromise;
|
|
1309
|
-
return loginPromise;
|
|
1310
|
-
};
|
|
1311
|
-
const loadPublishedSkillCatalog = async ({ directory, useCache, signal, }) => {
|
|
1312
|
-
const workspaceResolution = await resolveWorkspace({
|
|
1313
|
-
config,
|
|
1314
|
-
directory,
|
|
1315
|
-
});
|
|
1316
|
-
const directoryPath = workspaceResolution.directoryPath;
|
|
1317
|
-
const cacheKey = workspaceResolution.cacheKey;
|
|
1318
|
-
const cached = cache.get(cacheKey);
|
|
1319
|
-
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1320
|
-
return {
|
|
1321
|
-
directoryPath,
|
|
1322
|
-
workspaceResolution,
|
|
1323
|
-
fetchResult: {
|
|
1324
|
-
...cached.result,
|
|
1325
|
-
source: 'cache',
|
|
1326
|
-
},
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
const inflight = catalogInflight.get(cacheKey);
|
|
1330
|
-
if (inflight) {
|
|
1331
|
-
return inflight;
|
|
1332
|
-
}
|
|
1333
|
-
const requestPromise = (async () => {
|
|
1334
|
-
const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
|
|
1335
|
-
cache.set(cacheKey, {
|
|
1336
|
-
result: fetchResult,
|
|
1337
|
-
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
1338
|
-
});
|
|
1339
|
-
return {
|
|
1340
|
-
directoryPath,
|
|
1341
|
-
workspaceResolution,
|
|
1342
|
-
fetchResult,
|
|
1343
|
-
};
|
|
1344
|
-
})();
|
|
1345
|
-
catalogInflight.set(cacheKey, requestPromise);
|
|
1346
|
-
try {
|
|
1347
|
-
return await requestPromise;
|
|
1348
|
-
}
|
|
1349
|
-
finally {
|
|
1350
|
-
catalogInflight.delete(cacheKey);
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
const loadPublishedSkillDetail = async ({ workspaceResolution, item, signal, useCache, }) => {
|
|
1354
|
-
const directoryPath = workspaceResolution.directoryPath;
|
|
1355
|
-
const cacheKey = getDetailCacheKey(workspaceResolution, item.skillVersion.id);
|
|
1356
|
-
const cached = detailCache.get(cacheKey);
|
|
1357
|
-
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1358
|
-
return {
|
|
1359
|
-
ok: true,
|
|
1360
|
-
detail: toPublishedSkillDetail({
|
|
1361
|
-
...item,
|
|
1362
|
-
publishedArtifact: cached.artifact,
|
|
1363
|
-
}),
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
const inflight = detailInflight.get(cacheKey);
|
|
1367
|
-
if (inflight) {
|
|
1368
|
-
return inflight;
|
|
1369
|
-
}
|
|
1370
|
-
const requestPromise = (async () => {
|
|
1371
|
-
const detailResult = await fetchPublishedSkillDetail({
|
|
1372
|
-
worktree: input.worktree,
|
|
1373
|
-
config,
|
|
1374
|
-
resolution: workspaceResolution,
|
|
1375
|
-
skillVersionId: item.skillVersion.id,
|
|
1376
|
-
signal,
|
|
1377
|
-
onAuthStateChanged: clearPublishedSkillState,
|
|
1378
|
-
});
|
|
1379
|
-
if (!detailResult.ok) {
|
|
1380
|
-
return {
|
|
1381
|
-
ok: false,
|
|
1382
|
-
status: detailResult.result.status,
|
|
1383
|
-
output: JSON.stringify({
|
|
1384
|
-
pluginId: PLUGIN_ID,
|
|
1385
|
-
runtimeMode: 'tool_fetch_only',
|
|
1386
|
-
status: detailResult.result.status,
|
|
1387
|
-
requestedDirectoryPath: directoryPath,
|
|
1388
|
-
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
1389
|
-
requestedSkillVersionId: item.skillVersion.id,
|
|
1390
|
-
message: detailResult.result.message,
|
|
1391
|
-
fetchedAt: detailResult.result.fetchedAt,
|
|
1392
|
-
source: detailResult.result.source,
|
|
1393
|
-
}, null, 2),
|
|
1394
|
-
metadata: {
|
|
1395
|
-
status: detailResult.result.status,
|
|
1396
|
-
...toWorkspaceResolutionMetadata(workspaceResolution),
|
|
1397
|
-
source: detailResult.result.source,
|
|
1398
|
-
},
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
detailCache.set(cacheKey, {
|
|
1402
|
-
artifact: detailResult.artifact,
|
|
1403
|
-
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
1404
|
-
});
|
|
1405
|
-
return {
|
|
1406
|
-
ok: true,
|
|
1407
|
-
detail: toPublishedSkillDetail({
|
|
1408
|
-
...item,
|
|
1409
|
-
publishedArtifact: detailResult.artifact,
|
|
1410
|
-
}),
|
|
1411
|
-
};
|
|
1412
|
-
})();
|
|
1413
|
-
detailInflight.set(cacheKey, requestPromise);
|
|
1414
|
-
try {
|
|
1415
|
-
return await requestPromise;
|
|
1416
|
-
}
|
|
1417
|
-
finally {
|
|
1418
|
-
detailInflight.delete(cacheKey);
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
const executePublishedSkillsFetchTool = async ({ args, context, }) => {
|
|
1422
|
-
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
1423
|
-
const requestedSkills = parseRequestedSkillArgs(args);
|
|
1424
|
-
if (requestedSkills.length === 0) {
|
|
1425
|
-
return {
|
|
1426
|
-
output: JSON.stringify({
|
|
1427
|
-
pluginId: PLUGIN_ID,
|
|
1428
|
-
runtimeMode: 'tool_fetch_only',
|
|
1429
|
-
status: 'invalid_request',
|
|
1430
|
-
message: 'Provide `skill` for one request or `skills` for one or more comma/newline-separated requests. Passive/system context lists the published skills available for the current scope.',
|
|
1431
|
-
}, null, 2),
|
|
1432
|
-
metadata: {
|
|
1433
|
-
status: 'invalid_request',
|
|
1434
|
-
},
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
|
|
1438
|
-
lastInteractiveDirectoryPath = fetchActionDirectoryPath;
|
|
1439
|
-
const emitFetchOutcome = async (event) => {
|
|
1440
|
-
await emitActionEventForCurrentSession({
|
|
1441
|
-
event,
|
|
1442
|
-
directoryPath: fetchActionDirectoryPath,
|
|
1443
|
-
});
|
|
1444
|
-
};
|
|
1445
|
-
let publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1446
|
-
directory: requestedDirectory,
|
|
1447
|
-
useCache: !args.refresh,
|
|
1448
|
-
signal: context.abort,
|
|
1449
|
-
});
|
|
1450
|
-
if (publishedSkillsResult.fetchResult.ok) {
|
|
1451
|
-
await scheduleInteractivePresenceStart();
|
|
1452
|
-
}
|
|
1453
|
-
if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
|
|
1454
|
-
try {
|
|
1455
|
-
await startLoginCompletion('fetch').then(async (authState) => {
|
|
1456
|
-
await schedulePresenceStart(authState);
|
|
1457
|
-
});
|
|
1458
|
-
publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1459
|
-
directory: requestedDirectory,
|
|
1460
|
-
useCache: false,
|
|
1461
|
-
signal: context.abort,
|
|
1462
|
-
});
|
|
1463
|
-
if (publishedSkillsResult.fetchResult.ok) {
|
|
1464
|
-
await scheduleInteractivePresenceStart();
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
catch {
|
|
1468
|
-
// Return the original fetch failure with the latest login bootstrap snapshot attached.
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
if (!publishedSkillsResult.fetchResult.ok) {
|
|
1472
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
1473
|
-
return toFetchFailureOutput({
|
|
1474
|
-
worktree: input.worktree,
|
|
1475
|
-
config,
|
|
1476
|
-
publishedSkillsResult,
|
|
1477
|
-
loginBootstrapSnapshot: loginBootstrap.snapshot,
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
const selection = selectPublishedSkills(publishedSkillsResult.fetchResult.payload, requestedSkills);
|
|
1481
|
-
const isSingleRequest = requestedSkills.length === 1;
|
|
1482
|
-
if (selection.selectedItems.length === 0 && isSingleRequest) {
|
|
1483
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
1484
|
-
return {
|
|
1485
|
-
output: JSON.stringify({
|
|
1486
|
-
pluginId: PLUGIN_ID,
|
|
1487
|
-
runtimeMode: 'tool_fetch_only',
|
|
1488
|
-
status: 'not_found',
|
|
1489
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1490
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1491
|
-
requestedSkill: requestedSkills[0],
|
|
1492
|
-
availableSkills: publishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
|
|
1493
|
-
}, null, 2),
|
|
1494
|
-
metadata: {
|
|
1495
|
-
status: 'not_found',
|
|
1496
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1497
|
-
},
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
let skillDetailResults = await Promise.all(selection.selectedItems.map((item) => loadPublishedSkillDetail({
|
|
1501
|
-
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
1502
|
-
item,
|
|
1503
|
-
signal: context.abort,
|
|
1504
|
-
useCache: !args.refresh,
|
|
1505
|
-
})));
|
|
1506
|
-
if (skillDetailResults.some((result) => !result.ok && result.status === 'missing_auth')) {
|
|
1507
|
-
try {
|
|
1508
|
-
await startLoginCompletion('fetch').then(async (authState) => {
|
|
1509
|
-
await schedulePresenceStart(authState);
|
|
1510
|
-
});
|
|
1511
|
-
skillDetailResults = await Promise.all(selection.selectedItems.map((item) => loadPublishedSkillDetail({
|
|
1512
|
-
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
1513
|
-
item,
|
|
1514
|
-
signal: context.abort,
|
|
1515
|
-
useCache: false,
|
|
1516
|
-
})));
|
|
1517
|
-
}
|
|
1518
|
-
catch {
|
|
1519
|
-
// Return the original detail failure after the login bootstrap attempt updates snapshot state.
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
const failedSkillDetail = skillDetailResults.find((result) => !result.ok);
|
|
1523
|
-
if (failedSkillDetail && !failedSkillDetail.ok) {
|
|
1524
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
1525
|
-
return failedSkillDetail;
|
|
1526
|
-
}
|
|
1527
|
-
const skillDetails = skillDetailResults.map((result) => {
|
|
1528
|
-
if (!result.ok) {
|
|
1529
|
-
throw new Error('Published skill detail result unexpectedly missing after success guard.');
|
|
1530
|
-
}
|
|
1531
|
-
return result.detail;
|
|
1532
|
-
});
|
|
1533
|
-
if (isSingleRequest && skillDetails[0]) {
|
|
1534
|
-
const detail = skillDetails[0];
|
|
1535
|
-
context.metadata({
|
|
1536
|
-
title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
|
|
1537
|
-
metadata: {
|
|
1538
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1539
|
-
skillSlug: detail.skillSlug,
|
|
1540
|
-
version: detail.version,
|
|
1541
|
-
},
|
|
1542
|
-
});
|
|
1543
|
-
await emitFetchOutcome('FETCH_SUCCESS');
|
|
1544
|
-
return {
|
|
1545
|
-
output: JSON.stringify({
|
|
1546
|
-
pluginId: PLUGIN_ID,
|
|
1547
|
-
runtimeMode: 'tool_fetch_only',
|
|
1548
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1549
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1550
|
-
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1551
|
-
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1552
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
1553
|
-
skill: detail,
|
|
1554
|
-
}, null, 2),
|
|
1555
|
-
metadata: {
|
|
1556
|
-
status: 'ready',
|
|
1557
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1558
|
-
skillSlug: detail.skillSlug,
|
|
1559
|
-
},
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
context.metadata({
|
|
1563
|
-
title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
|
|
1564
|
-
metadata: {
|
|
1565
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1566
|
-
requestedCount: requestedSkills.length.toString(),
|
|
1567
|
-
matchedCount: skillDetails.length.toString(),
|
|
1568
|
-
},
|
|
1569
|
-
});
|
|
1570
|
-
await emitFetchOutcome('FETCH_SUCCESS');
|
|
1571
|
-
return {
|
|
1572
|
-
output: JSON.stringify({
|
|
1573
|
-
pluginId: PLUGIN_ID,
|
|
1574
|
-
runtimeMode: 'tool_fetch_only',
|
|
1575
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
1576
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
1577
|
-
workspace: publishedSkillsResult.fetchResult.payload.workspace,
|
|
1578
|
-
fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
|
|
1579
|
-
source: publishedSkillsResult.fetchResult.source,
|
|
1580
|
-
requestedSkills,
|
|
1581
|
-
missingSkills: selection.missingIdentifiers,
|
|
1582
|
-
skills: skillDetails,
|
|
1583
|
-
}, null, 2),
|
|
1584
|
-
metadata: {
|
|
1585
|
-
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
1586
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1587
|
-
matchedCount: skillDetails.length.toString(),
|
|
1588
|
-
},
|
|
1589
|
-
};
|
|
1590
|
-
};
|
|
1591
|
-
return {
|
|
1592
|
-
tool: {
|
|
1593
|
-
opencode_wizard_published_skills_fetch: tool({
|
|
1594
|
-
description: 'Fetch one or multiple published skill bodies/details for the current scope',
|
|
1595
|
-
args: {
|
|
1596
|
-
skill: tool.schema.string().optional().describe('Single skill slug, artifact name, or skill name'),
|
|
1597
|
-
skills: tool.schema
|
|
1598
|
-
.string()
|
|
1599
|
-
.optional()
|
|
1600
|
-
.describe('One or more comma-separated or newline-separated skill slugs, artifact names, or skill names'),
|
|
1601
|
-
directory: tool.schema.string().optional().describe('Optional absolute or relative directory override'),
|
|
1602
|
-
refresh: tool.schema.boolean().optional().describe('Bypass the local plugin cache for this request'),
|
|
1603
|
-
},
|
|
1604
|
-
async execute(args, context) {
|
|
1605
|
-
return executePublishedSkillsFetchTool({
|
|
1606
|
-
args,
|
|
1607
|
-
context,
|
|
1608
|
-
});
|
|
1609
|
-
},
|
|
1610
|
-
}),
|
|
1611
|
-
},
|
|
1612
|
-
'experimental.chat.system.transform': async (_hookInput, output) => {
|
|
1613
|
-
const publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
1614
|
-
directory: input.directory,
|
|
1615
|
-
useCache: true,
|
|
1616
|
-
signal: AbortSignal.timeout(5_000),
|
|
1617
|
-
});
|
|
1618
|
-
const systemNote = buildSystemNote(publishedSkillsResult, config);
|
|
1619
|
-
if (!systemNote)
|
|
1620
|
-
return;
|
|
1621
|
-
output.system.push(systemNote);
|
|
1622
|
-
},
|
|
1623
|
-
};
|
|
1624
|
-
};
|
|
1625
|
-
export default {
|
|
1626
|
-
id: PLUGIN_ID,
|
|
1627
|
-
server: OpencodeWizardSkillsPlugin,
|
|
1628
|
-
};
|