@aexol/opencode-wizard 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graphql-operations.d.ts +4 -0
- package/dist/graphql-operations.js +225 -0
- package/dist/graphql-operations.js.map +1 -0
- package/dist/plugin-tools.d.ts +64 -0
- package/dist/plugin-tools.js +57 -0
- package/dist/plugin-tools.js.map +1 -0
- package/dist/published-skills-system-note.d.ts +9 -0
- package/dist/published-skills-system-note.js +34 -0
- package/dist/published-skills-system-note.js.map +1 -0
- package/dist/published-skills-transform.d.ts +161 -0
- package/dist/published-skills-transform.js +238 -0
- package/dist/published-skills-transform.js.map +1 -0
- package/dist/server/auth-flow.d.ts +10 -0
- package/dist/server/auth-flow.js +215 -0
- package/dist/server/auth-flow.js.map +1 -0
- package/dist/server/auth-store.d.ts +19 -0
- package/dist/server/auth-store.js +177 -0
- package/dist/server/auth-store.js.map +1 -0
- package/dist/server/client.d.ts +51 -0
- package/dist/server/client.js +244 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/config.js +82 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/constants.d.ts +26 -0
- package/dist/server/constants.js +32 -0
- package/dist/server/constants.js.map +1 -0
- package/dist/server/path-utils.d.ts +2 -0
- package/dist/server/path-utils.js +8 -0
- package/dist/server/path-utils.js.map +1 -0
- package/dist/server/presence.d.ts +14 -0
- package/dist/server/presence.js +68 -0
- package/dist/server/presence.js.map +1 -0
- package/dist/server/runtime.d.ts +32 -0
- package/dist/server/runtime.js +1110 -0
- package/dist/server/runtime.js.map +1 -0
- package/dist/server/status.d.ts +27 -0
- package/dist/server/status.js +224 -0
- package/dist/server/status.js.map +1 -0
- package/dist/server/types.d.ts +321 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/workspace.d.ts +15 -0
- package/dist/server/workspace.js +126 -0
- package/dist/server/workspace.js.map +1 -0
- package/dist/server.d.ts +4 -309
- package/dist/server.js +4 -2611
- package/dist/server.js.map +1 -1
- package/dist/smoke-published-skills.js +11 -9
- package/dist/smoke-published-skills.js.map +1 -1
- package/dist/tui/components/common.d.ts +15 -0
- package/dist/tui/components/common.js +81 -0
- package/dist/tui/components/common.js.map +1 -0
- package/dist/tui/components/preference-action-notice-row.d.ts +5 -0
- package/dist/tui/components/preference-action-notice-row.js +17 -0
- package/dist/tui/components/preference-action-notice-row.js.map +1 -0
- package/dist/tui/components/skill-catalog-row.d.ts +8 -0
- package/dist/tui/components/skill-catalog-row.js +124 -0
- package/dist/tui/components/skill-catalog-row.js.map +1 -0
- package/dist/tui/components/status-content.d.ts +14 -0
- package/dist/tui/components/status-content.js +131 -0
- package/dist/tui/components/status-content.js.map +1 -0
- package/dist/tui/components/wizard-skills-dialog-content.d.ts +9 -0
- package/dist/tui/components/wizard-skills-dialog-content.js +219 -0
- package/dist/tui/components/wizard-skills-dialog-content.js.map +1 -0
- package/dist/tui/components/wizard-skills-dialog.d.ts +7 -0
- package/dist/tui/components/wizard-skills-dialog.js +156 -0
- package/dist/tui/components/wizard-skills-dialog.js.map +1 -0
- package/dist/tui/constants.d.ts +8 -0
- package/dist/tui/constants.js +9 -0
- package/dist/tui/constants.js.map +1 -0
- package/dist/tui/formatting.d.ts +8 -0
- package/dist/tui/formatting.js +45 -0
- package/dist/tui/formatting.js.map +1 -0
- package/dist/tui/plugin.d.ts +2 -0
- package/dist/tui/plugin.js +26 -0
- package/dist/tui/plugin.js.map +1 -0
- package/dist/tui/rendering.d.ts +2 -0
- package/dist/tui/rendering.js +8 -0
- package/dist/tui/rendering.js.map +1 -0
- package/dist/tui/skill-helpers.d.ts +13 -0
- package/dist/tui/skill-helpers.js +49 -0
- package/dist/tui/skill-helpers.js.map +1 -0
- package/dist/tui/slots.d.ts +2 -0
- package/dist/tui/slots.js +56 -0
- package/dist/tui/slots.js.map +1 -0
- package/dist/tui/status.d.ts +2 -0
- package/dist/tui/status.js +21 -0
- package/dist/tui/status.js.map +1 -0
- package/dist/tui/types.d.ts +75 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/tui.d.ts +1 -44
- package/dist/tui.js +2 -870
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,2614 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import { execFile } from 'node:child_process';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
|
-
import { URL, fileURLToPath } from 'node:url';
|
|
9
|
-
import { resolveBackendOriginFromValues } from './config.js';
|
|
10
|
-
import { sendOAuthCallbackHtmlResponse } from './oauth-callback-page.js';
|
|
11
|
-
import { deleteFileIfExists, readJsonFile, writePrivateJsonFile } from './storage.js';
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
13
|
-
const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
|
|
14
|
-
const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
|
|
15
|
-
export const PLUGIN_ID = 'opencode-wizard';
|
|
16
|
-
const CACHE_TTL_MS = 30_000;
|
|
17
|
-
const WORKSPACE_MAPPING_LIMIT = 100;
|
|
18
|
-
const ROOT_SKILL_SEED_PATH = '.opencode/skills';
|
|
19
|
-
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
|
|
20
|
-
const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
|
|
21
|
-
const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
|
|
22
|
-
const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
|
|
23
|
-
const OIDC_CALLBACK_ORIGIN = 'http://localhost:24953';
|
|
24
|
-
const OIDC_CALLBACK_PATH = '/oauth/callback';
|
|
25
|
-
const OIDC_CALLBACK_URL = `${OIDC_CALLBACK_ORIGIN}${OIDC_CALLBACK_PATH}`;
|
|
26
|
-
const OIDC_SCOPES = ['openid', 'profile', 'email'];
|
|
27
|
-
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
28
|
-
const SYSTEM_NOTE_SKILL_NAME_LIMIT = 10;
|
|
29
|
-
const SYSTEM_NOTE_DETAIL_LIMIT = 3;
|
|
30
|
-
const SYSTEM_NOTE_DETAIL_CHAR_LIMIT = 2_400;
|
|
31
|
-
const SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT = 140;
|
|
32
|
-
const PRESENCE_EVENT_TIMEOUT_MS = 3_000;
|
|
33
|
-
const PRESENCE_EVENT_MAX_ATTEMPTS = 2;
|
|
34
|
-
const PRESENCE_EVENT_RETRY_DELAY_MS = 250;
|
|
35
|
-
const PRESENCE_SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
36
|
-
const PRESENCE_SIGNAL_EXIT_CODES = {
|
|
37
|
-
SIGINT: 130,
|
|
38
|
-
SIGTERM: 143,
|
|
39
|
-
SIGHUP: 129
|
|
40
|
-
};
|
|
41
|
-
const createIdleLoginBootstrapSnapshot = () => ({
|
|
42
|
-
status: 'idle',
|
|
43
|
-
trigger: null,
|
|
44
|
-
startedAt: null,
|
|
45
|
-
expiresAt: null,
|
|
46
|
-
browserUrl: null,
|
|
47
|
-
browserOpenError: null,
|
|
48
|
-
email: null,
|
|
49
|
-
message: null
|
|
50
|
-
});
|
|
51
|
-
const STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS = 60_000;
|
|
52
|
-
const statusPathLoginBootstrap = {
|
|
53
|
-
promise: null,
|
|
54
|
-
status: 'idle',
|
|
55
|
-
message: null,
|
|
56
|
-
failedAt: null
|
|
57
|
-
};
|
|
58
|
-
const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
|
|
59
|
-
export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set', 'opencode_wizard_status'];
|
|
60
|
-
let publishedSkillPreferenceCacheVersion = 0;
|
|
61
|
-
export const NATIVE_SKILLS_URL_COMPATIBILITY = {
|
|
62
|
-
configKey: 'skills.urls',
|
|
63
|
-
deliveryMode: 'public_static_registry',
|
|
64
|
-
wizardPrivateDelivery: 'authenticated_scoped_fetch_tool',
|
|
65
|
-
authSupport: 'none',
|
|
66
|
-
guidance: 'OpenCode skills.urls is for public/static registries and complements opencode-wizard; private workspace-scoped skills stay available through the authenticated fetch tool only.'
|
|
67
|
-
};
|
|
68
|
-
const PUBLISHED_SKILLS_CATALOG_QUERY = `
|
|
69
|
-
query PluginPublishedSkills($input: PublishedSkillsDeliveryInput!) {
|
|
70
|
-
pluginPublishedSkills(input: $input) {
|
|
71
|
-
workspace {
|
|
72
|
-
id
|
|
73
|
-
slug
|
|
74
|
-
name
|
|
75
|
-
repositoryUrl
|
|
76
|
-
defaultBranch
|
|
77
|
-
status
|
|
78
|
-
}
|
|
79
|
-
directoryPath
|
|
80
|
-
skills {
|
|
81
|
-
assignmentSource
|
|
82
|
-
assignmentType
|
|
83
|
-
scopePath
|
|
84
|
-
includeChildren
|
|
85
|
-
skill {
|
|
86
|
-
id
|
|
87
|
-
slug
|
|
88
|
-
name
|
|
89
|
-
summary
|
|
90
|
-
whenToUse
|
|
91
|
-
status
|
|
92
|
-
installPolicy
|
|
93
|
-
tags {
|
|
94
|
-
id
|
|
95
|
-
slug
|
|
96
|
-
label
|
|
97
|
-
description
|
|
98
|
-
facet {
|
|
99
|
-
id
|
|
100
|
-
slug
|
|
101
|
-
label
|
|
102
|
-
description
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
skillVersion {
|
|
107
|
-
id
|
|
108
|
-
version
|
|
109
|
-
title
|
|
110
|
-
summary
|
|
111
|
-
status
|
|
112
|
-
}
|
|
113
|
-
publishedArtifact {
|
|
114
|
-
id
|
|
115
|
-
frontmatterName
|
|
116
|
-
frontmatterDescription
|
|
117
|
-
checksum
|
|
118
|
-
publishedAt
|
|
119
|
-
fileCount
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catalogSkills {
|
|
123
|
-
skill {
|
|
124
|
-
id
|
|
125
|
-
slug
|
|
126
|
-
name
|
|
127
|
-
summary
|
|
128
|
-
whenToUse
|
|
129
|
-
status
|
|
130
|
-
installPolicy
|
|
131
|
-
tags {
|
|
132
|
-
id
|
|
133
|
-
slug
|
|
134
|
-
label
|
|
135
|
-
description
|
|
136
|
-
facet {
|
|
137
|
-
id
|
|
138
|
-
slug
|
|
139
|
-
label
|
|
140
|
-
description
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
skillVersion {
|
|
145
|
-
id
|
|
146
|
-
version
|
|
147
|
-
title
|
|
148
|
-
summary
|
|
149
|
-
status
|
|
150
|
-
}
|
|
151
|
-
publishedArtifact {
|
|
152
|
-
id
|
|
153
|
-
frontmatterName
|
|
154
|
-
frontmatterDescription
|
|
155
|
-
checksum
|
|
156
|
-
publishedAt
|
|
157
|
-
fileCount
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
userPreferences {
|
|
161
|
-
scopeKey
|
|
162
|
-
userKey
|
|
163
|
-
ignoredSkills {
|
|
164
|
-
assignmentSource
|
|
165
|
-
assignmentType
|
|
166
|
-
scopePath
|
|
167
|
-
includeChildren
|
|
168
|
-
skill {
|
|
169
|
-
id
|
|
170
|
-
slug
|
|
171
|
-
name
|
|
172
|
-
summary
|
|
173
|
-
whenToUse
|
|
174
|
-
status
|
|
175
|
-
installPolicy
|
|
176
|
-
tags {
|
|
177
|
-
id
|
|
178
|
-
slug
|
|
179
|
-
label
|
|
180
|
-
description
|
|
181
|
-
facet {
|
|
182
|
-
id
|
|
183
|
-
slug
|
|
184
|
-
label
|
|
185
|
-
description
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
skillVersion {
|
|
190
|
-
id
|
|
191
|
-
version
|
|
192
|
-
title
|
|
193
|
-
summary
|
|
194
|
-
status
|
|
195
|
-
}
|
|
196
|
-
publishedArtifact {
|
|
197
|
-
id
|
|
198
|
-
frontmatterName
|
|
199
|
-
frontmatterDescription
|
|
200
|
-
checksum
|
|
201
|
-
publishedAt
|
|
202
|
-
fileCount
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
`;
|
|
209
|
-
const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
|
|
210
|
-
mutation SetPublishedSkillPreference($input: SetPublishedSkillPreferenceInput!) {
|
|
211
|
-
setPublishedSkillPreference(input: $input) {
|
|
212
|
-
scopeKey
|
|
213
|
-
userKey
|
|
214
|
-
ignoredSkills {
|
|
215
|
-
assignmentSource
|
|
216
|
-
assignmentType
|
|
217
|
-
scopePath
|
|
218
|
-
includeChildren
|
|
219
|
-
skill {
|
|
220
|
-
id
|
|
221
|
-
slug
|
|
222
|
-
name
|
|
223
|
-
summary
|
|
224
|
-
whenToUse
|
|
225
|
-
status
|
|
226
|
-
installPolicy
|
|
227
|
-
tags {
|
|
228
|
-
id
|
|
229
|
-
slug
|
|
230
|
-
label
|
|
231
|
-
description
|
|
232
|
-
facet {
|
|
233
|
-
id
|
|
234
|
-
slug
|
|
235
|
-
label
|
|
236
|
-
description
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
skillVersion {
|
|
241
|
-
id
|
|
242
|
-
version
|
|
243
|
-
title
|
|
244
|
-
summary
|
|
245
|
-
status
|
|
246
|
-
}
|
|
247
|
-
publishedArtifact {
|
|
248
|
-
id
|
|
249
|
-
frontmatterName
|
|
250
|
-
frontmatterDescription
|
|
251
|
-
checksum
|
|
252
|
-
publishedAt
|
|
253
|
-
fileCount
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
`;
|
|
259
|
-
const PUBLISHED_SKILL_DETAIL_QUERY = `
|
|
260
|
-
query PluginPublishedSkillVersionArtifact($input: PublishedSkillArtifactDetailInput!) {
|
|
261
|
-
pluginPublishedSkillVersionArtifact(input: $input) {
|
|
262
|
-
id
|
|
263
|
-
frontmatterName
|
|
264
|
-
frontmatterDescription
|
|
265
|
-
markdownBody
|
|
266
|
-
renderedContent
|
|
267
|
-
checksum
|
|
268
|
-
publishedAt
|
|
269
|
-
fileCount
|
|
270
|
-
files {
|
|
271
|
-
id
|
|
272
|
-
relativePath
|
|
273
|
-
contentType
|
|
274
|
-
content
|
|
275
|
-
checksum
|
|
276
|
-
size
|
|
277
|
-
sortOrder
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
`;
|
|
282
|
-
const parseDotEnvValue = value => {
|
|
283
|
-
const trimmedValue = value.trim();
|
|
284
|
-
if (trimmedValue.startsWith('"') && trimmedValue.endsWith('"') || trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
|
|
285
|
-
return trimmedValue.slice(1, -1);
|
|
286
|
-
}
|
|
287
|
-
return trimmedValue;
|
|
288
|
-
};
|
|
289
|
-
const parseDotEnv = raw => {
|
|
290
|
-
const values = new Map();
|
|
291
|
-
for (const line of raw.split(/\r?\n/u)) {
|
|
292
|
-
const trimmedLine = line.trim();
|
|
293
|
-
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
294
|
-
const separatorIndex = trimmedLine.indexOf('=');
|
|
295
|
-
if (separatorIndex <= 0) continue;
|
|
296
|
-
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
297
|
-
if (!key) continue;
|
|
298
|
-
const rawValue = trimmedLine.slice(separatorIndex + 1);
|
|
299
|
-
values.set(key, parseDotEnvValue(rawValue));
|
|
300
|
-
}
|
|
301
|
-
return values;
|
|
302
|
-
};
|
|
303
|
-
const findUpwardFile = async (startDirectory, fileName) => {
|
|
304
|
-
let currentDirectory = normalizeAbsolutePath(startDirectory);
|
|
305
|
-
while (true) {
|
|
306
|
-
const candidatePath = path.join(currentDirectory, fileName);
|
|
307
|
-
try {
|
|
308
|
-
await fs.access(candidatePath);
|
|
309
|
-
return candidatePath;
|
|
310
|
-
} catch {
|
|
311
|
-
const parentDirectory = path.dirname(currentDirectory);
|
|
312
|
-
if (parentDirectory === currentDirectory) return null;
|
|
313
|
-
currentDirectory = parentDirectory;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
const readLocalEnvValues = async startDirectory => {
|
|
318
|
-
const envPath = await findUpwardFile(startDirectory, '.env');
|
|
319
|
-
if (!envPath) return new Map();
|
|
320
|
-
try {
|
|
321
|
-
const raw = await fs.readFile(envPath, 'utf8');
|
|
322
|
-
return parseDotEnv(raw);
|
|
323
|
-
} catch {
|
|
324
|
-
return new Map();
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
const resolveBackendOrigin = async worktree => {
|
|
328
|
-
const envValues = await readLocalEnvValues(worktree);
|
|
329
|
-
return resolveBackendOriginFromValues({
|
|
330
|
-
environmentBackendOrigin: process.env.OPENCODE_WIZARD_BACKEND_ORIGIN,
|
|
331
|
-
localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
|
|
332
|
-
});
|
|
333
|
-
};
|
|
334
|
-
const readConfiguredWorkspaceSlug = () => {
|
|
335
|
-
const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
|
|
336
|
-
if (!configuredWorkspaceSlug) return null;
|
|
337
|
-
return toWorkspaceSlug(configuredWorkspaceSlug);
|
|
338
|
-
};
|
|
339
|
-
const toWorkspaceSlug = value => {
|
|
340
|
-
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
341
|
-
if (normalized) return normalized;
|
|
342
|
-
return 'workspace';
|
|
343
|
-
};
|
|
344
|
-
const resolveFallbackWorkspaceSlug = worktree => {
|
|
345
|
-
const configuredWorkspaceSlug = readConfiguredWorkspaceSlug();
|
|
346
|
-
if (configuredWorkspaceSlug) return configuredWorkspaceSlug;
|
|
347
|
-
return toWorkspaceSlug(path.basename(path.resolve(worktree)));
|
|
348
|
-
};
|
|
349
|
-
export const resolveConfig = async worktree => {
|
|
350
|
-
const backendOrigin = await resolveBackendOrigin(worktree);
|
|
351
|
-
return {
|
|
352
|
-
backendOrigin,
|
|
353
|
-
graphqlUrl: `${backendOrigin}/graphql`,
|
|
354
|
-
authSessionUrl: `${backendOrigin}/api/opencode-plugin/oauth/session`,
|
|
355
|
-
presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
|
|
356
|
-
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
357
|
-
configuredWorkspaceSlug: readConfiguredWorkspaceSlug(),
|
|
358
|
-
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
359
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
360
|
-
authStatePath: GLOBAL_CONFIG_PATH
|
|
361
|
-
};
|
|
362
|
-
};
|
|
363
|
-
const normalizeAbsolutePath = value => path.resolve(value);
|
|
364
|
-
const normalizeRepositoryPath = (worktree, directory) => {
|
|
365
|
-
const absoluteWorktree = normalizeAbsolutePath(worktree);
|
|
366
|
-
const absoluteDirectory = normalizeAbsolutePath(directory);
|
|
367
|
-
const relativePath = path.relative(absoluteWorktree, absoluteDirectory);
|
|
368
|
-
if (!relativePath || relativePath === '') return '.';
|
|
369
|
-
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) return '.';
|
|
370
|
-
return relativePath.split(path.sep).join('/');
|
|
371
|
-
};
|
|
372
|
-
const tryExecGit = async args => {
|
|
373
|
-
try {
|
|
374
|
-
const {
|
|
375
|
-
stdout
|
|
376
|
-
} = await execFileAsync('git', args, {
|
|
377
|
-
encoding: 'utf8'
|
|
378
|
-
});
|
|
379
|
-
const normalizedOutput = stdout.trim();
|
|
380
|
-
if (normalizedOutput) return normalizedOutput;
|
|
381
|
-
return null;
|
|
382
|
-
} catch {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
const resolveGitRoot = async directory => {
|
|
387
|
-
const gitRoot = await tryExecGit(['-C', directory, 'rev-parse', '--show-toplevel']);
|
|
388
|
-
if (!gitRoot) return null;
|
|
389
|
-
return normalizeAbsolutePath(gitRoot);
|
|
390
|
-
};
|
|
391
|
-
const normalizeGitRemoteUrl = remoteUrl => {
|
|
392
|
-
if (!remoteUrl) return null;
|
|
393
|
-
const trimmedRemoteUrl = remoteUrl.trim();
|
|
394
|
-
if (!trimmedRemoteUrl) return null;
|
|
395
|
-
const scpLikeMatch = /^git@([^:]+):(.+)$/u.exec(trimmedRemoteUrl);
|
|
396
|
-
if (scpLikeMatch) {
|
|
397
|
-
return `ssh://git@${scpLikeMatch[1]}/${scpLikeMatch[2].replace(/^\/+/, '')}`;
|
|
398
|
-
}
|
|
399
|
-
try {
|
|
400
|
-
const parsedUrl = new URL(trimmedRemoteUrl);
|
|
401
|
-
return parsedUrl.toString().replace(/\/+$/u, '');
|
|
402
|
-
} catch {
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
const resolveGitRemoteOriginUrl = async repositoryRoot => {
|
|
407
|
-
const remoteUrl = await tryExecGit(['-C', repositoryRoot, 'remote', 'get-url', 'origin']);
|
|
408
|
-
return normalizeGitRemoteUrl(remoteUrl);
|
|
409
|
-
};
|
|
410
|
-
const resolveWorkspace = async ({
|
|
411
|
-
config,
|
|
412
|
-
directory
|
|
413
|
-
}) => {
|
|
414
|
-
const requestedDirectory = normalizeAbsolutePath(directory);
|
|
415
|
-
const gitRoot = await resolveGitRoot(requestedDirectory);
|
|
416
|
-
const repositoryRoot = gitRoot ?? requestedDirectory;
|
|
417
|
-
const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
|
|
418
|
-
const learnedWorkspaceMapping = await findWorkspaceSlugMapping({
|
|
419
|
-
configFile: config.authStatePath,
|
|
420
|
-
repositoryUrl,
|
|
421
|
-
repositoryRoot
|
|
422
|
-
});
|
|
423
|
-
const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
|
|
424
|
-
const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
|
|
425
|
-
const workspaceSlug = config.configuredWorkspaceSlug ?? learnedWorkspaceMapping?.workspaceSlug ?? fallbackWorkspaceSlug ?? null;
|
|
426
|
-
const workspaceSlugSource = config.configuredWorkspaceSlug ? 'configured' : learnedWorkspaceMapping?.workspaceSlug ? 'learned' : fallbackWorkspaceSlug ? 'fallback' : repositoryUrl ? 'repositoryUrl' : 'placeholder';
|
|
427
|
-
const workspaceIdentity = workspaceSlug ? `workspaceSlug:${workspaceSlug}` : repositoryUrl ? `repository:${repositoryUrl}` : 'workspace:placeholder';
|
|
428
|
-
return {
|
|
429
|
-
requestedDirectory,
|
|
430
|
-
repositoryRoot,
|
|
431
|
-
repositoryUrl,
|
|
432
|
-
workspaceSlug,
|
|
433
|
-
workspaceSlugSource,
|
|
434
|
-
fallbackWorkspaceSlug,
|
|
435
|
-
directoryPath,
|
|
436
|
-
cacheKey: JSON.stringify([workspaceIdentity, directoryPath])
|
|
437
|
-
};
|
|
438
|
-
};
|
|
439
|
-
const toDeliveryInput = resolution => {
|
|
440
|
-
if (resolution.workspaceSlug) {
|
|
441
|
-
return {
|
|
442
|
-
workspaceSlug: resolution.workspaceSlug,
|
|
443
|
-
directoryPath: resolution.directoryPath
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
if (resolution.repositoryUrl) {
|
|
447
|
-
return {
|
|
448
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
449
|
-
directoryPath: resolution.directoryPath
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
return {
|
|
453
|
-
workspaceSlug: 'workspace',
|
|
454
|
-
directoryPath: resolution.directoryPath
|
|
455
|
-
};
|
|
456
|
-
};
|
|
457
|
-
const formatSkillLabel = item => {
|
|
458
|
-
const artifactName = item.publishedArtifact.frontmatterName.trim();
|
|
459
|
-
if (artifactName.length > 0) return artifactName;
|
|
460
|
-
return item.skill.name;
|
|
461
|
-
};
|
|
462
|
-
const toFrontmatterString = value => JSON.stringify(value);
|
|
463
|
-
const isRecord = value => {
|
|
464
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
465
|
-
};
|
|
466
|
-
const isValidIsoDateString = value => {
|
|
467
|
-
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
468
|
-
};
|
|
469
|
-
const isAuthState = value => {
|
|
470
|
-
if (!isRecord(value)) return false;
|
|
471
|
-
return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
|
|
472
|
-
};
|
|
473
|
-
const readGlobalConfig = async configFile => {
|
|
474
|
-
const storedConfig = await readJsonFile(configFile);
|
|
475
|
-
if (isRecord(storedConfig)) return storedConfig;
|
|
476
|
-
return {};
|
|
477
|
-
};
|
|
478
|
-
const writeGlobalConfig = async (configFile, config) => {
|
|
479
|
-
await writePrivateJsonFile(configFile, config);
|
|
480
|
-
};
|
|
481
|
-
const withoutLegacyPublishedSkillPreferences = config => {
|
|
482
|
-
const {
|
|
483
|
-
publishedSkillPreferences,
|
|
484
|
-
ignoredPublishedSkills,
|
|
485
|
-
...safeConfig
|
|
486
|
-
} = config;
|
|
487
|
-
void publishedSkillPreferences;
|
|
488
|
-
void ignoredPublishedSkills;
|
|
489
|
-
return safeConfig;
|
|
490
|
-
};
|
|
491
|
-
const hasLegacyPublishedSkillPreferences = config => {
|
|
492
|
-
return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
|
|
493
|
-
};
|
|
494
|
-
const isStoredWorkspaceSlugMapping = value => {
|
|
495
|
-
if (!isRecord(value)) return false;
|
|
496
|
-
const {
|
|
497
|
-
repositoryUrl,
|
|
498
|
-
repositoryRoot,
|
|
499
|
-
workspaceSlug,
|
|
500
|
-
updatedAt
|
|
501
|
-
} = value;
|
|
502
|
-
const hasValidRepositoryUrl = repositoryUrl === null || typeof repositoryUrl === 'string';
|
|
503
|
-
const hasValidRepositoryRoot = repositoryRoot === null || typeof repositoryRoot === 'string';
|
|
504
|
-
return hasValidRepositoryUrl && hasValidRepositoryRoot && typeof workspaceSlug === 'string' && workspaceSlug.trim().length > 0 && isValidIsoDateString(updatedAt);
|
|
505
|
-
};
|
|
506
|
-
const readWorkspaceSlugMappings = async configFile => {
|
|
507
|
-
const storedConfig = await readGlobalConfig(configFile);
|
|
508
|
-
const mappings = storedConfig.workspaceSlugMappings;
|
|
509
|
-
if (!Array.isArray(mappings)) return [];
|
|
510
|
-
return mappings.filter(isStoredWorkspaceSlugMapping).slice(0, WORKSPACE_MAPPING_LIMIT);
|
|
511
|
-
};
|
|
512
|
-
const writeWorkspaceSlugMappings = async (configFile, nextMappings) => {
|
|
513
|
-
const storedConfig = await readGlobalConfig(configFile);
|
|
514
|
-
await writeGlobalConfig(configFile, {
|
|
515
|
-
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
516
|
-
workspaceSlugMappings: nextMappings.slice(0, WORKSPACE_MAPPING_LIMIT)
|
|
517
|
-
});
|
|
518
|
-
};
|
|
519
|
-
const normalizeStoredRepositoryRoot = value => {
|
|
520
|
-
if (!value) return null;
|
|
521
|
-
return normalizeAbsolutePath(value);
|
|
522
|
-
};
|
|
523
|
-
const upsertWorkspaceSlugMapping = async ({
|
|
524
|
-
configFile,
|
|
525
|
-
repositoryUrl,
|
|
526
|
-
repositoryRoot,
|
|
527
|
-
workspaceSlug
|
|
528
|
-
}) => {
|
|
529
|
-
const normalizedWorkspaceSlug = toWorkspaceSlug(workspaceSlug);
|
|
530
|
-
if (!normalizedWorkspaceSlug) return;
|
|
531
|
-
const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
|
|
532
|
-
const existingMappings = await readWorkspaceSlugMappings(configFile);
|
|
533
|
-
const filteredMappings = existingMappings.filter(mapping => {
|
|
534
|
-
if (repositoryUrl && mapping.repositoryUrl === repositoryUrl) return false;
|
|
535
|
-
if (normalizedRepositoryRoot && normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot) {
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
538
|
-
return true;
|
|
539
|
-
});
|
|
540
|
-
await writeWorkspaceSlugMappings(configFile, [{
|
|
541
|
-
repositoryUrl,
|
|
542
|
-
repositoryRoot: normalizedRepositoryRoot,
|
|
543
|
-
workspaceSlug: normalizedWorkspaceSlug,
|
|
544
|
-
updatedAt: new Date().toISOString()
|
|
545
|
-
}, ...filteredMappings]);
|
|
546
|
-
};
|
|
547
|
-
const findWorkspaceSlugMapping = async ({
|
|
548
|
-
configFile,
|
|
549
|
-
repositoryUrl,
|
|
550
|
-
repositoryRoot
|
|
551
|
-
}) => {
|
|
552
|
-
const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
|
|
553
|
-
const mappings = await readWorkspaceSlugMappings(configFile);
|
|
554
|
-
if (repositoryUrl) {
|
|
555
|
-
const repositoryMatch = mappings.find(mapping => mapping.repositoryUrl === repositoryUrl);
|
|
556
|
-
if (repositoryMatch) return repositoryMatch;
|
|
557
|
-
}
|
|
558
|
-
if (normalizedRepositoryRoot) {
|
|
559
|
-
const rootMatch = mappings.find(mapping => normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot);
|
|
560
|
-
if (rootMatch) return rootMatch;
|
|
561
|
-
}
|
|
562
|
-
return null;
|
|
563
|
-
};
|
|
564
|
-
const readGlobalAuthState = async configFile => {
|
|
565
|
-
const storedConfig = await readGlobalConfig(configFile);
|
|
566
|
-
const storedAuthState = storedConfig.auth;
|
|
567
|
-
if (storedAuthState === undefined || storedAuthState === null) return null;
|
|
568
|
-
if (isAuthState(storedAuthState)) {
|
|
569
|
-
if (hasLegacyPublishedSkillPreferences(storedConfig)) {
|
|
570
|
-
await writeGlobalConfig(configFile, withoutLegacyPublishedSkillPreferences(storedConfig));
|
|
571
|
-
}
|
|
572
|
-
return storedAuthState;
|
|
573
|
-
}
|
|
574
|
-
await writeGlobalConfig(configFile, {
|
|
575
|
-
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
576
|
-
auth: null
|
|
577
|
-
});
|
|
578
|
-
return null;
|
|
579
|
-
};
|
|
580
|
-
const readLegacyAuthState = async authStateFile => {
|
|
581
|
-
const storedAuthState = await readJsonFile(authStateFile);
|
|
582
|
-
if (storedAuthState === null) return null;
|
|
583
|
-
if (isAuthState(storedAuthState)) return storedAuthState;
|
|
584
|
-
await deleteFileIfExists(authStateFile);
|
|
585
|
-
return null;
|
|
586
|
-
};
|
|
587
|
-
const writeAuthState = async (configFile, authState) => {
|
|
588
|
-
const storedConfig = await readGlobalConfig(configFile);
|
|
589
|
-
await writeGlobalConfig(configFile, {
|
|
590
|
-
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
591
|
-
auth: authState
|
|
592
|
-
});
|
|
593
|
-
};
|
|
594
|
-
const clearAuthState = async configFile => {
|
|
595
|
-
const storedConfig = await readGlobalConfig(configFile);
|
|
596
|
-
await writeGlobalConfig(configFile, {
|
|
597
|
-
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
598
|
-
auth: null
|
|
599
|
-
});
|
|
600
|
-
};
|
|
601
|
-
const toIgnoredSkillSlug = value => {
|
|
602
|
-
const normalized = value.trim().toLowerCase();
|
|
603
|
-
if (!normalized) return null;
|
|
604
|
-
return normalized;
|
|
605
|
-
};
|
|
606
|
-
const toPublishedSkillPreferenceAction = value => {
|
|
607
|
-
const normalized = value.trim().toLowerCase();
|
|
608
|
-
if (normalized === 'install' || normalized === 'uninstall' || normalized === 'ignore' || normalized === 'unignore') {
|
|
609
|
-
return normalized;
|
|
610
|
-
}
|
|
611
|
-
throw new Error('Published skill preference action must be one of: install, uninstall, ignore, unignore.');
|
|
612
|
-
};
|
|
613
|
-
const toPublishedSkillPreferenceScope = (value, defaultScope) => {
|
|
614
|
-
if (!value) return defaultScope;
|
|
615
|
-
const normalized = value.trim().toLowerCase();
|
|
616
|
-
if (normalized === 'global' || normalized === 'project') return normalized;
|
|
617
|
-
throw new Error('Published skill preferenceScope must be either global or project.');
|
|
618
|
-
};
|
|
619
|
-
const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
|
|
620
|
-
const workspaceSlug = payload?.workspace?.slug ?? resolution.workspaceSlug ?? resolution.fallbackWorkspaceSlug;
|
|
621
|
-
if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
|
|
622
|
-
if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
|
|
623
|
-
return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
|
|
624
|
-
};
|
|
625
|
-
const toStoredUserKey = authState => {
|
|
626
|
-
if (authState?.userId) return authState.userId;
|
|
627
|
-
if (authState?.email) return authState.email.toLowerCase();
|
|
628
|
-
return 'anonymous';
|
|
629
|
-
};
|
|
630
|
-
const resolvePublishedSkillPreferenceCacheContext = async config => {
|
|
631
|
-
const authState = await readGlobalAuthState(config.authStatePath);
|
|
632
|
-
return {
|
|
633
|
-
userKey: toStoredUserKey(authState),
|
|
634
|
-
preferenceVersion: publishedSkillPreferenceCacheVersion
|
|
635
|
-
};
|
|
636
|
-
};
|
|
637
|
-
const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
|
|
638
|
-
return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
|
|
639
|
-
};
|
|
640
|
-
const toAuthState = session => ({
|
|
641
|
-
pluginId: PLUGIN_ID,
|
|
642
|
-
sessionToken: session.jwtToken,
|
|
643
|
-
expiresAt: session.expiresAt,
|
|
644
|
-
authenticatedAt: new Date().toISOString(),
|
|
645
|
-
userId: session.user.id,
|
|
646
|
-
email: session.user.email
|
|
647
|
-
});
|
|
648
|
-
const resolveStoredAuthState = async (worktree, config) => {
|
|
649
|
-
const authState = await readGlobalAuthState(config.authStatePath);
|
|
650
|
-
if (authState && Date.parse(authState.expiresAt) > Date.now()) {
|
|
651
|
-
return authState;
|
|
652
|
-
}
|
|
653
|
-
if (authState) {
|
|
654
|
-
await clearAuthState(config.authStatePath);
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
const legacyAuthStateFile = path.resolve(worktree, LEGACY_AUTH_STATE_PATH);
|
|
658
|
-
const legacyAuthState = await readLegacyAuthState(legacyAuthStateFile);
|
|
659
|
-
if (!legacyAuthState) return null;
|
|
660
|
-
if (Date.parse(legacyAuthState.expiresAt) <= Date.now()) {
|
|
661
|
-
await deleteFileIfExists(legacyAuthStateFile);
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
664
|
-
await writeAuthState(config.authStatePath, legacyAuthState);
|
|
665
|
-
await deleteFileIfExists(legacyAuthStateFile);
|
|
666
|
-
return legacyAuthState;
|
|
667
|
-
};
|
|
668
|
-
export const buildSkillMarkdown = item => {
|
|
669
|
-
const artifactBody = item.publishedArtifact.markdownBody.trim();
|
|
670
|
-
const fallbackBody = item.publishedArtifact.renderedContent.trim();
|
|
671
|
-
const body = artifactBody || fallbackBody;
|
|
672
|
-
if (body.startsWith('---')) {
|
|
673
|
-
return body.endsWith('\n') ? body : `${body}\n`;
|
|
674
|
-
}
|
|
675
|
-
const name = formatSkillLabel(item);
|
|
676
|
-
const description = item.publishedArtifact.frontmatterDescription.trim() || item.skill.summary?.trim() || '';
|
|
677
|
-
const frontmatter = ['---', `name: ${toFrontmatterString(name)}`, `description: ${toFrontmatterString(description)}`, '---'].join('\n');
|
|
678
|
-
if (!body) {
|
|
679
|
-
return `${frontmatter}\n`;
|
|
680
|
-
}
|
|
681
|
-
return `${frontmatter}\n\n${body.endsWith('\n') ? body : `${body}\n`}`;
|
|
682
|
-
};
|
|
683
|
-
const getSkillIdentifiers = item => {
|
|
684
|
-
const candidates = [item.skill.slug, item.publishedArtifact.frontmatterName, item.skill.name];
|
|
685
|
-
const seen = new Set();
|
|
686
|
-
return candidates.reduce((all, candidate) => {
|
|
687
|
-
const normalized = candidate.trim();
|
|
688
|
-
if (!normalized) return all;
|
|
689
|
-
const cacheKey = normalized.toLowerCase();
|
|
690
|
-
if (seen.has(cacheKey)) return all;
|
|
691
|
-
seen.add(cacheKey);
|
|
692
|
-
all.push(normalized);
|
|
693
|
-
return all;
|
|
694
|
-
}, []);
|
|
695
|
-
};
|
|
696
|
-
const toPublishedSkillFacetSummary = facet => ({
|
|
697
|
-
slug: facet.slug,
|
|
698
|
-
label: facet.label,
|
|
699
|
-
description: facet.description ?? null
|
|
700
|
-
});
|
|
701
|
-
const toPublishedSkillTagSummary = tag => ({
|
|
702
|
-
slug: tag.slug,
|
|
703
|
-
label: tag.label,
|
|
704
|
-
description: tag.description ?? null,
|
|
705
|
-
facet: tag.facet ? toPublishedSkillFacetSummary(tag.facet) : null
|
|
706
|
-
});
|
|
707
|
-
const getPublishedSkillFacets = items => {
|
|
708
|
-
const facetsBySlug = new Map();
|
|
709
|
-
for (const item of items) {
|
|
710
|
-
for (const tag of item.skill.tags) {
|
|
711
|
-
if (!tag.facet) continue;
|
|
712
|
-
if (facetsBySlug.has(tag.facet.slug)) continue;
|
|
713
|
-
facetsBySlug.set(tag.facet.slug, toPublishedSkillFacetSummary(tag.facet));
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return [...facetsBySlug.values()].sort((left, right) => left.slug.localeCompare(right.slug));
|
|
717
|
-
};
|
|
718
|
-
const isUserPublishedSkillAssignment = assignmentSource => assignmentSource === 'USER' || assignmentSource === 'USER_GLOBAL' || assignmentSource === 'USER_WORKSPACE';
|
|
719
|
-
const getPublishedSkillAssignmentLabel = assignmentSource => {
|
|
720
|
-
if (assignmentSource === 'GLOBAL') return 'GLOBAL SCOPE assignment';
|
|
721
|
-
if (assignmentSource === 'WORKSPACE') return 'PROJECT SCOPE assignment';
|
|
722
|
-
if (assignmentSource === 'USER_GLOBAL') return 'USER SCOPE preference (global target)';
|
|
723
|
-
if (assignmentSource === 'USER_WORKSPACE') return 'USER SCOPE preference (project target)';
|
|
724
|
-
if (assignmentSource === 'USER') return 'USER SCOPE assignment';
|
|
725
|
-
return `${assignmentSource.toUpperCase().replace(/_/gu, ' ')} assignment`;
|
|
726
|
-
};
|
|
727
|
-
const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) => {
|
|
728
|
-
if (isUserPublishedSkillAssignment(item.assignmentSource)) {
|
|
729
|
-
return {
|
|
730
|
-
...counts,
|
|
731
|
-
user: counts.user + 1
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
if (item.assignmentSource === 'GLOBAL') {
|
|
735
|
-
return {
|
|
736
|
-
...counts,
|
|
737
|
-
global: counts.global + 1
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
if (item.assignmentSource === 'WORKSPACE') {
|
|
741
|
-
return {
|
|
742
|
-
...counts,
|
|
743
|
-
project: counts.project + 1
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
...counts,
|
|
748
|
-
other: counts.other + 1
|
|
749
|
-
};
|
|
750
|
-
}, {
|
|
751
|
-
global: 0,
|
|
752
|
-
project: 0,
|
|
753
|
-
user: 0,
|
|
754
|
-
other: 0
|
|
755
|
-
});
|
|
756
|
-
const getSkillContextKind = item => {
|
|
757
|
-
if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
|
|
758
|
-
return 'project';
|
|
759
|
-
};
|
|
760
|
-
const getSkillPolicyLabel = (policy, contextKind, assignmentSource) => {
|
|
761
|
-
if (isUserPublishedSkillAssignment(assignmentSource) && policy === 'GLOBAL_CONTEXT') {
|
|
762
|
-
return 'GLOBAL_CONTEXT · active USER SCOPE context';
|
|
763
|
-
}
|
|
764
|
-
if (isUserPublishedSkillAssignment(assignmentSource)) return 'PROJECT_INSTALLABLE · active USER SCOPE preference';
|
|
765
|
-
if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
|
|
766
|
-
if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
|
|
767
|
-
if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active GLOBAL SCOPE assignment';
|
|
768
|
-
return 'PROJECT_INSTALLABLE · active PROJECT SCOPE assignment';
|
|
769
|
-
};
|
|
770
|
-
const toPublishedSkillSummary = item => {
|
|
771
|
-
const contextKind = getSkillContextKind(item);
|
|
772
|
-
return {
|
|
773
|
-
skillSlug: item.skill.slug,
|
|
774
|
-
skillName: item.skill.name,
|
|
775
|
-
artifactName: item.publishedArtifact.frontmatterName,
|
|
776
|
-
artifactDescription: item.publishedArtifact.frontmatterDescription,
|
|
777
|
-
whenToUse: item.skill.whenToUse ?? null,
|
|
778
|
-
version: item.skillVersion.version,
|
|
779
|
-
assignmentSource: item.assignmentSource,
|
|
780
|
-
assignmentType: item.assignmentType,
|
|
781
|
-
scopePath: item.scopePath,
|
|
782
|
-
includeChildren: item.includeChildren ?? null,
|
|
783
|
-
checksum: item.publishedArtifact.checksum,
|
|
784
|
-
publishedAt: item.publishedArtifact.publishedAt,
|
|
785
|
-
fileCount: item.publishedArtifact.fileCount,
|
|
786
|
-
identifiers: getSkillIdentifiers(item),
|
|
787
|
-
tags: item.skill.tags.map(toPublishedSkillTagSummary),
|
|
788
|
-
contextKind,
|
|
789
|
-
installPolicy: item.skill.installPolicy,
|
|
790
|
-
assignmentLabel: getPublishedSkillAssignmentLabel(item.assignmentSource),
|
|
791
|
-
policyLabel: getSkillPolicyLabel(item.skill.installPolicy, contextKind, item.assignmentSource)
|
|
792
|
-
};
|
|
793
|
-
};
|
|
794
|
-
export const toPublishedSkillDetail = item => ({
|
|
795
|
-
...toPublishedSkillSummary(item),
|
|
796
|
-
skillId: item.skill.id,
|
|
797
|
-
skillVersionId: item.skillVersion.id,
|
|
798
|
-
artifactId: item.publishedArtifact.id,
|
|
799
|
-
markdownDocument: buildSkillMarkdown(item),
|
|
800
|
-
markdownBody: item.publishedArtifact.markdownBody,
|
|
801
|
-
renderedContent: item.publishedArtifact.renderedContent,
|
|
802
|
-
files: item.publishedArtifact.files,
|
|
803
|
-
resources: item.publishedArtifact.files.filter(file => file.relativePath !== 'SKILL.md')
|
|
804
|
-
});
|
|
805
|
-
const toInstallableSkillSummary = item => ({
|
|
806
|
-
skillSlug: item.skill.slug,
|
|
807
|
-
skillName: item.skill.name,
|
|
808
|
-
artifactName: item.publishedArtifact.frontmatterName,
|
|
809
|
-
artifactDescription: item.publishedArtifact.frontmatterDescription,
|
|
810
|
-
whenToUse: item.skill.whenToUse ?? null,
|
|
811
|
-
version: item.skillVersion.version,
|
|
812
|
-
assignmentSource: 'CATALOG',
|
|
813
|
-
assignmentType: 'PATH',
|
|
814
|
-
scopePath: '',
|
|
815
|
-
includeChildren: true,
|
|
816
|
-
checksum: item.publishedArtifact.checksum,
|
|
817
|
-
publishedAt: item.publishedArtifact.publishedAt,
|
|
818
|
-
fileCount: item.publishedArtifact.fileCount,
|
|
819
|
-
identifiers: getSkillIdentifiers({
|
|
820
|
-
...item,
|
|
821
|
-
assignmentSource: 'CATALOG',
|
|
822
|
-
assignmentType: 'PATH',
|
|
823
|
-
scopePath: '',
|
|
824
|
-
includeChildren: true
|
|
825
|
-
}),
|
|
826
|
-
tags: item.skill.tags.map(toPublishedSkillTagSummary),
|
|
827
|
-
contextKind: 'installable',
|
|
828
|
-
installPolicy: item.skill.installPolicy,
|
|
829
|
-
assignmentLabel: 'catalog skill',
|
|
830
|
-
policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable', 'CATALOG')
|
|
831
|
-
});
|
|
832
|
-
export const toPublishedSkillCatalog = payload => ({
|
|
833
|
-
pluginId: PLUGIN_ID,
|
|
834
|
-
runtimeMode: 'tool_fetch_only',
|
|
835
|
-
deliveryModel: 'backend_published_installed_effective_skills',
|
|
836
|
-
workspace: payload.workspace,
|
|
837
|
-
directoryPath: payload.directoryPath,
|
|
838
|
-
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
839
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
840
|
-
publishedSkillCount: payload.skills.length,
|
|
841
|
-
assignmentCounts: getPublishedSkillAssignmentCounts(payload.skills),
|
|
842
|
-
facets: getPublishedSkillFacets(payload.skills),
|
|
843
|
-
skills: payload.skills.map(toPublishedSkillSummary)
|
|
844
|
-
});
|
|
845
|
-
const filterIgnoredPublishedSkills = async (config, result) => {
|
|
846
|
-
const authState = await readGlobalAuthState(config.authStatePath);
|
|
847
|
-
const userKey = toStoredUserKey(authState);
|
|
848
|
-
if (!result.fetchResult.ok) {
|
|
849
|
-
return {
|
|
850
|
-
...result,
|
|
851
|
-
ignoreState: {
|
|
852
|
-
scopeKey: getPublishedSkillIgnoreScopeKey(result.workspaceResolution),
|
|
853
|
-
userKey,
|
|
854
|
-
ignoredSkillSlugs: [],
|
|
855
|
-
installedGlobalSkillSlugs: [],
|
|
856
|
-
installedWorkspaceSkillSlugs: []
|
|
857
|
-
},
|
|
858
|
-
ignoredSkills: []
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
const ignoredSkills = result.fetchResult.payload.userPreferences.ignoredSkills.map(toPublishedSkillSummary);
|
|
862
|
-
const ignoredSkillSlugs = ignoredSkills.map(skill => skill.skillSlug);
|
|
863
|
-
return {
|
|
864
|
-
...result,
|
|
865
|
-
ignoreState: {
|
|
866
|
-
scopeKey: result.fetchResult.payload.userPreferences.scopeKey,
|
|
867
|
-
userKey: result.fetchResult.payload.userPreferences.userKey || userKey,
|
|
868
|
-
ignoredSkillSlugs,
|
|
869
|
-
installedGlobalSkillSlugs: [],
|
|
870
|
-
installedWorkspaceSkillSlugs: []
|
|
871
|
-
},
|
|
872
|
-
ignoredSkills
|
|
873
|
-
};
|
|
874
|
-
};
|
|
875
|
-
const getWorkspaceUnavailableMessage = payload => {
|
|
876
|
-
if (payload.workspace) return null;
|
|
877
|
-
return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
|
|
878
|
-
};
|
|
879
|
-
const normalizeSkillIdentifier = value => value.trim().toLowerCase();
|
|
880
|
-
const parseSkillIdentifiers = value => {
|
|
881
|
-
const seen = new Set();
|
|
882
|
-
return value.split(/[\n,]/).map(item => item.trim()).filter(item => item.length > 0).filter(item => {
|
|
883
|
-
const normalized = normalizeSkillIdentifier(item);
|
|
884
|
-
if (seen.has(normalized)) return false;
|
|
885
|
-
seen.add(normalized);
|
|
886
|
-
return true;
|
|
887
|
-
});
|
|
888
|
-
};
|
|
889
|
-
const mergeSkillIdentifiers = values => {
|
|
890
|
-
const seen = new Set();
|
|
891
|
-
return values.filter(value => {
|
|
892
|
-
const normalized = normalizeSkillIdentifier(value);
|
|
893
|
-
if (!normalized || seen.has(normalized)) return false;
|
|
894
|
-
seen.add(normalized);
|
|
895
|
-
return true;
|
|
896
|
-
});
|
|
897
|
-
};
|
|
898
|
-
export const parseRequestedSkillArgs = args => {
|
|
899
|
-
const requestedSkills = [];
|
|
900
|
-
if (typeof args.skill === 'string') {
|
|
901
|
-
requestedSkills.push(...parseSkillIdentifiers(args.skill));
|
|
902
|
-
}
|
|
903
|
-
if (typeof args.skills === 'string') {
|
|
904
|
-
requestedSkills.push(...parseSkillIdentifiers(args.skills));
|
|
905
|
-
}
|
|
906
|
-
return mergeSkillIdentifiers(requestedSkills);
|
|
907
|
-
};
|
|
908
|
-
export const selectPublishedSkills = (payload, identifiers) => {
|
|
909
|
-
const itemsByIdentifier = new Map();
|
|
910
|
-
for (const item of payload.skills) {
|
|
911
|
-
for (const identifier of getSkillIdentifiers(item)) {
|
|
912
|
-
itemsByIdentifier.set(normalizeSkillIdentifier(identifier), item);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
const selectedItems = [];
|
|
916
|
-
const selectedKeys = new Set();
|
|
917
|
-
const missingIdentifiers = [];
|
|
918
|
-
for (const identifier of identifiers) {
|
|
919
|
-
const matched = itemsByIdentifier.get(normalizeSkillIdentifier(identifier));
|
|
920
|
-
if (!matched) {
|
|
921
|
-
missingIdentifiers.push(identifier);
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
if (selectedKeys.has(matched.publishedArtifact.id)) {
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
selectedKeys.add(matched.publishedArtifact.id);
|
|
928
|
-
selectedItems.push(matched);
|
|
929
|
-
}
|
|
930
|
-
return {
|
|
931
|
-
selectedItems,
|
|
932
|
-
missingIdentifiers
|
|
933
|
-
};
|
|
934
|
-
};
|
|
935
|
-
const truncateText = (value, maxLength) => {
|
|
936
|
-
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
937
|
-
if (normalized.length <= maxLength) return normalized;
|
|
938
|
-
return `${normalized.slice(0, Math.max(maxLength - 1, 0)).trimEnd()}…`;
|
|
939
|
-
};
|
|
940
|
-
const buildSkillCatalogLine = skill => {
|
|
941
|
-
const description = truncateText(skill.whenToUse || skill.artifactDescription || skill.skillName || skill.skillSlug, SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT);
|
|
942
|
-
const scopeLabel = isUserPublishedSkillAssignment(skill.assignmentSource) ? 'USER SCOPE' : skill.contextKind === 'global' ? 'GLOBAL SCOPE' : 'PROJECT SCOPE';
|
|
943
|
-
const assignmentLabel = skill.assignmentSource.toLowerCase().replace(/_/gu, ' ');
|
|
944
|
-
return `- ${skill.artifactName || skill.skillName} (${skill.skillSlug}, ${assignmentLabel} assignment) [${scopeLabel}]: ${description}`;
|
|
945
|
-
};
|
|
946
|
-
const buildSkillDetailSnippetLine = detail => {
|
|
947
|
-
const body = detail.markdownBody || detail.renderedContent || detail.markdownDocument;
|
|
948
|
-
return `- ${detail.artifactName || detail.skillName}: ${truncateText(body, 700)}`;
|
|
949
|
-
};
|
|
950
|
-
export const buildSystemNote = (result, config, details) => {
|
|
951
|
-
if (!result.fetchResult.ok) return null;
|
|
952
|
-
const catalog = toPublishedSkillCatalog(result.fetchResult.payload);
|
|
953
|
-
const skillNames = catalog.skills.map(skill => skill.artifactName || skill.skillName || skill.skillSlug);
|
|
954
|
-
const renderedSkillNames = skillNames.length > 0 ? skillNames.slice(0, SYSTEM_NOTE_SKILL_NAME_LIMIT).join(', ') : 'none';
|
|
955
|
-
const remainingCount = Math.max(skillNames.length - SYSTEM_NOTE_SKILL_NAME_LIMIT, 0);
|
|
956
|
-
const renderedCountSuffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
|
|
957
|
-
const globalSkills = catalog.skills.filter(skill => skill.contextKind === 'global' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 8).map(buildSkillCatalogLine);
|
|
958
|
-
const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
|
|
959
|
-
const userSkills = catalog.skills.filter(skill => isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
|
|
960
|
-
const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
|
|
961
|
-
const detailBlock = detailLines.length > 0 ? `Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
|
|
962
|
-
return [result.fetchResult.payload.workspace ? `Workspace: ${result.fetchResult.payload.workspace.slug}.` : 'Workspace not found; workspace-scoped wizard skills are unavailable.', `Current directory: ${result.directoryPath}.`, `Active wizard skills: ${renderedSkillNames}${renderedCountSuffix}.`, `Counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.user} user, ${catalog.assignmentCounts.other} other.`, 'Wizard-listed skills are backend-published, not native OpenCode skills.', 'Use native skill tooling only for names in native available_skills.', 'When a wizard skill matches by whenToUse, fetch its current body with opencode_wizard_published_skills_fetch before using it.', 'Use `skills` for multiple wizard skill identifiers; use `skill` for one.', 'If native skill loading cannot find a wizard-listed skill, fetch it as a wizard skill instead.', 'Fetched wizard bodies are authoritative over local seed/native sources.', globalSkills.length > 0 ? `Global skills:\n${globalSkills.join('\n')}` : 'Global skills: none.', projectSkills.length > 0 ? `Project skills:\n${projectSkills.join('\n')}` : 'Project skills: none.', userSkills.length > 0 ? `User skills:\n${userSkills.join('\n')}` : 'User skills: none.', detailBlock].filter(Boolean).join(' ');
|
|
963
|
-
};
|
|
964
|
-
const toWorkspaceResolutionOutput = resolution => ({
|
|
965
|
-
requestedDirectory: resolution.requestedDirectory,
|
|
966
|
-
repositoryRoot: resolution.repositoryRoot,
|
|
967
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
968
|
-
workspaceSlug: resolution.workspaceSlug,
|
|
969
|
-
workspaceSlugSource: resolution.workspaceSlugSource,
|
|
970
|
-
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
971
|
-
directoryPath: resolution.directoryPath
|
|
972
|
-
});
|
|
973
|
-
const toWorkspaceResolutionMetadata = resolution => ({
|
|
974
|
-
directoryPath: resolution.directoryPath,
|
|
975
|
-
repositoryRoot: resolution.repositoryRoot,
|
|
976
|
-
repositoryUrl: resolution.repositoryUrl ?? '',
|
|
977
|
-
workspaceSlug: resolution.workspaceSlug ?? '',
|
|
978
|
-
workspaceSlugSource: resolution.workspaceSlugSource ?? 'placeholder',
|
|
979
|
-
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? ''
|
|
980
|
-
});
|
|
981
|
-
const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
|
|
982
|
-
const authState = await resolveStoredAuthState(worktree, config);
|
|
983
|
-
const filteredResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
984
|
-
const base = {
|
|
985
|
-
pluginId: PLUGIN_ID,
|
|
986
|
-
runtimeMode: 'tool_fetch_only',
|
|
987
|
-
nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
|
|
988
|
-
backendOrigin: config.backendOrigin,
|
|
989
|
-
graphqlUrl: config.graphqlUrl,
|
|
990
|
-
fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
|
|
991
|
-
workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
|
|
992
|
-
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
993
|
-
authStatePath: config.authStatePath,
|
|
994
|
-
requestedDirectoryPath: publishedSkillsResult.directoryPath,
|
|
995
|
-
authMode: publishedSkillsResult.fetchResult.authMode,
|
|
996
|
-
authState: authState === null ? null : {
|
|
997
|
-
email: authState.email,
|
|
998
|
-
userId: authState.userId,
|
|
999
|
-
authenticatedAt: authState.authenticatedAt,
|
|
1000
|
-
expiresAt: authState.expiresAt
|
|
1001
|
-
},
|
|
1002
|
-
loginBootstrap: loginBootstrapSnapshot.status === 'idle' ? null : {
|
|
1003
|
-
status: loginBootstrapSnapshot.status,
|
|
1004
|
-
trigger: loginBootstrapSnapshot.trigger,
|
|
1005
|
-
startedAt: loginBootstrapSnapshot.startedAt,
|
|
1006
|
-
expiresAt: loginBootstrapSnapshot.expiresAt,
|
|
1007
|
-
browserUrl: loginBootstrapSnapshot.browserUrl,
|
|
1008
|
-
browserOpenError: loginBootstrapSnapshot.browserOpenError,
|
|
1009
|
-
email: loginBootstrapSnapshot.email,
|
|
1010
|
-
message: loginBootstrapSnapshot.message
|
|
1011
|
-
},
|
|
1012
|
-
status: filteredResult.fetchResult.status,
|
|
1013
|
-
fetchedAt: filteredResult.fetchResult.fetchedAt,
|
|
1014
|
-
source: filteredResult.fetchResult.source,
|
|
1015
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
1016
|
-
ignoredPublishedSkills: {
|
|
1017
|
-
scopeKey: filteredResult.ignoreState.scopeKey,
|
|
1018
|
-
userKey: filteredResult.ignoreState.userKey,
|
|
1019
|
-
count: filteredResult.ignoreState.ignoredSkillSlugs.length
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
|
-
if (!filteredResult.fetchResult.ok) {
|
|
1023
|
-
return JSON.stringify({
|
|
1024
|
-
...base,
|
|
1025
|
-
message: filteredResult.fetchResult.message
|
|
1026
|
-
}, null, 2);
|
|
1027
|
-
}
|
|
1028
|
-
return JSON.stringify({
|
|
1029
|
-
...base,
|
|
1030
|
-
...toPublishedSkillCatalog(filteredResult.fetchResult.payload),
|
|
1031
|
-
message: getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload)
|
|
1032
|
-
}, null, 2);
|
|
1033
|
-
};
|
|
1034
|
-
export const toPluginAuthStateSummary = authState => {
|
|
1035
|
-
if (!authState) {
|
|
1036
|
-
return {
|
|
1037
|
-
status: 'missing',
|
|
1038
|
-
email: null,
|
|
1039
|
-
userId: null,
|
|
1040
|
-
authenticatedAt: null,
|
|
1041
|
-
expiresAt: null
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
return {
|
|
1045
|
-
status: 'authenticated',
|
|
1046
|
-
email: authState.email,
|
|
1047
|
-
userId: authState.userId,
|
|
1048
|
-
authenticatedAt: authState.authenticatedAt,
|
|
1049
|
-
expiresAt: authState.expiresAt
|
|
1050
|
-
};
|
|
1051
|
-
};
|
|
1052
|
-
export const resolvePluginStatusSnapshot = async ({
|
|
1053
|
-
worktree,
|
|
1054
|
-
directory,
|
|
1055
|
-
signal
|
|
1056
|
-
}) => {
|
|
1057
|
-
const config = await resolveConfig(worktree);
|
|
1058
|
-
const workspaceResolution = await resolveWorkspace({
|
|
1059
|
-
config,
|
|
1060
|
-
directory
|
|
1061
|
-
});
|
|
1062
|
-
const fetchResult = await fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal);
|
|
1063
|
-
const filteredResult = await filterIgnoredPublishedSkills(config, {
|
|
1064
|
-
directoryPath: workspaceResolution.directoryPath,
|
|
1065
|
-
workspaceResolution,
|
|
1066
|
-
fetchResult
|
|
1067
|
-
});
|
|
1068
|
-
const authState = await resolveStoredAuthState(worktree, config);
|
|
1069
|
-
return {
|
|
1070
|
-
pluginId: PLUGIN_ID,
|
|
1071
|
-
runtimeMode: 'tool_fetch_only',
|
|
1072
|
-
nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
|
|
1073
|
-
backendOrigin: config.backendOrigin,
|
|
1074
|
-
graphqlUrl: config.graphqlUrl,
|
|
1075
|
-
fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
|
|
1076
|
-
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
1077
|
-
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
1078
|
-
authStatePath: config.authStatePath,
|
|
1079
|
-
authState: toPluginAuthStateSummary(authState),
|
|
1080
|
-
status: filteredResult.fetchResult.status,
|
|
1081
|
-
authMode: filteredResult.fetchResult.authMode,
|
|
1082
|
-
fetchedAt: filteredResult.fetchResult.fetchedAt,
|
|
1083
|
-
source: filteredResult.fetchResult.source,
|
|
1084
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
1085
|
-
message: filteredResult.fetchResult.ok ? getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload) : filteredResult.fetchResult.message,
|
|
1086
|
-
catalog: filteredResult.fetchResult.ok ? toPublishedSkillCatalog(filteredResult.fetchResult.payload) : null,
|
|
1087
|
-
installableCatalog: filteredResult.fetchResult.ok ? {
|
|
1088
|
-
count: filteredResult.fetchResult.payload.catalogSkills.length,
|
|
1089
|
-
skills: filteredResult.fetchResult.payload.catalogSkills.map(toInstallableSkillSummary)
|
|
1090
|
-
} : null,
|
|
1091
|
-
ignoredPublishedSkills: {
|
|
1092
|
-
scopeKey: filteredResult.ignoreState.scopeKey,
|
|
1093
|
-
userKey: filteredResult.ignoreState.userKey,
|
|
1094
|
-
count: filteredResult.ignoreState.ignoredSkillSlugs.length,
|
|
1095
|
-
skills: filteredResult.ignoredSkills
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
};
|
|
1099
|
-
const withStatusMessage = (snapshot, message) => ({
|
|
1100
|
-
...snapshot,
|
|
1101
|
-
message
|
|
1102
|
-
});
|
|
1103
|
-
const toAiFacingPluginStatusSnapshot = snapshot => {
|
|
1104
|
-
const {
|
|
1105
|
-
ignoredPublishedSkills,
|
|
1106
|
-
installableCatalog: _installableCatalog,
|
|
1107
|
-
...safeSnapshot
|
|
1108
|
-
} = snapshot;
|
|
1109
|
-
return {
|
|
1110
|
-
...safeSnapshot,
|
|
1111
|
-
ignoredPublishedSkills: {
|
|
1112
|
-
scopeKey: ignoredPublishedSkills.scopeKey,
|
|
1113
|
-
count: ignoredPublishedSkills.count
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
1116
|
-
};
|
|
1117
|
-
const startStatusPathLoginBootstrap = (worktree, config) => {
|
|
1118
|
-
if (statusPathLoginBootstrap.promise) return;
|
|
1119
|
-
if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
statusPathLoginBootstrap.status = 'pending';
|
|
1123
|
-
statusPathLoginBootstrap.message = 'Browser login started automatically from the TUI/status path.';
|
|
1124
|
-
statusPathLoginBootstrap.failedAt = null;
|
|
1125
|
-
statusPathLoginBootstrap.promise = (async () => {
|
|
1126
|
-
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1127
|
-
const loginStart = await startLoginFlow(loginSignal);
|
|
1128
|
-
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1129
|
-
if (browserOpenError) {
|
|
1130
|
-
statusPathLoginBootstrap.message = `Automatic browser open failed. Open ${loginStart.browserUrl} manually.`;
|
|
1131
|
-
}
|
|
1132
|
-
try {
|
|
1133
|
-
const callbackPayload = await loginStart.callbackPromise;
|
|
1134
|
-
if (callbackPayload.status === 'error') {
|
|
1135
|
-
throw new Error(callbackPayload.message);
|
|
1136
|
-
}
|
|
1137
|
-
if (callbackPayload.state !== loginStart.expectedState) {
|
|
1138
|
-
throw new Error('OAuth callback state did not match the original login request.');
|
|
1139
|
-
}
|
|
1140
|
-
const pluginSession = await createPluginSession({
|
|
1141
|
-
code: callbackPayload.code,
|
|
1142
|
-
codeVerifier: loginStart.codeVerifier,
|
|
1143
|
-
redirectUri: OIDC_CALLBACK_URL,
|
|
1144
|
-
config,
|
|
1145
|
-
signal: loginSignal
|
|
1146
|
-
});
|
|
1147
|
-
const authState = toAuthState(pluginSession);
|
|
1148
|
-
await writeAuthState(config.authStatePath, authState);
|
|
1149
|
-
statusPathLoginBootstrap.status = 'authenticated';
|
|
1150
|
-
statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
|
|
1151
|
-
return authState;
|
|
1152
|
-
} finally {
|
|
1153
|
-
await loginStart.closeCallbackServer().catch(() => undefined);
|
|
1154
|
-
}
|
|
1155
|
-
})().catch(error => {
|
|
1156
|
-
statusPathLoginBootstrap.status = 'failed';
|
|
1157
|
-
statusPathLoginBootstrap.failedAt = Date.now();
|
|
1158
|
-
statusPathLoginBootstrap.message = error instanceof Error ? error.message : 'Browser login failed.';
|
|
1159
|
-
throw error;
|
|
1160
|
-
}).finally(() => {
|
|
1161
|
-
statusPathLoginBootstrap.promise = null;
|
|
1162
|
-
});
|
|
1163
|
-
statusPathLoginBootstrap.promise.catch(() => undefined);
|
|
1164
|
-
};
|
|
1165
|
-
export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
|
|
1166
|
-
worktree,
|
|
1167
|
-
directory,
|
|
1168
|
-
signal
|
|
1169
|
-
}) => {
|
|
1170
|
-
const snapshot = await resolvePluginStatusSnapshot({
|
|
1171
|
-
worktree,
|
|
1172
|
-
directory,
|
|
1173
|
-
signal
|
|
1174
|
-
});
|
|
1175
|
-
if (snapshot.status !== 'missing_auth') return snapshot;
|
|
1176
|
-
const config = await resolveConfig(worktree);
|
|
1177
|
-
startStatusPathLoginBootstrap(worktree, config);
|
|
1178
|
-
if (statusPathLoginBootstrap.message) {
|
|
1179
|
-
return withStatusMessage(snapshot, statusPathLoginBootstrap.message);
|
|
1180
|
-
}
|
|
1181
|
-
return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
|
|
1182
|
-
};
|
|
1183
|
-
const toBackendPreferenceScope = preferenceScope => {
|
|
1184
|
-
if (preferenceScope === 'global') return 'GLOBAL';
|
|
1185
|
-
return 'WORKSPACE';
|
|
1186
|
-
};
|
|
1187
|
-
const setPublishedSkillPreference = async ({
|
|
1188
|
-
worktree,
|
|
1189
|
-
directory,
|
|
1190
|
-
config,
|
|
1191
|
-
skillSlug,
|
|
1192
|
-
preferenceScope,
|
|
1193
|
-
installed,
|
|
1194
|
-
ignored
|
|
1195
|
-
}) => {
|
|
1196
|
-
const workspaceResolution = await resolveWorkspace({
|
|
1197
|
-
config,
|
|
1198
|
-
directory
|
|
1199
|
-
});
|
|
1200
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
1201
|
-
worktree,
|
|
1202
|
-
config,
|
|
1203
|
-
query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
|
|
1204
|
-
variables: {
|
|
1205
|
-
input: {
|
|
1206
|
-
...toDeliveryInput(workspaceResolution),
|
|
1207
|
-
skillSlug,
|
|
1208
|
-
preferenceScope: toBackendPreferenceScope(preferenceScope),
|
|
1209
|
-
installed,
|
|
1210
|
-
ignored
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1214
|
-
});
|
|
1215
|
-
if (!response.ok) {
|
|
1216
|
-
throw new Error(response.result.message);
|
|
1217
|
-
}
|
|
1218
|
-
const preferences = response.data.setPublishedSkillPreference;
|
|
1219
|
-
publishedSkillPreferenceCacheVersion += 1;
|
|
1220
|
-
return {
|
|
1221
|
-
scopeKey: preferences.scopeKey,
|
|
1222
|
-
userKey: preferences.userKey,
|
|
1223
|
-
ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
|
|
1224
|
-
installedGlobalSkillSlugs: [],
|
|
1225
|
-
installedWorkspaceSkillSlugs: []
|
|
1226
|
-
};
|
|
1227
|
-
};
|
|
1228
|
-
export const setPublishedSkillIgnored = async ({
|
|
1229
|
-
worktree,
|
|
1230
|
-
directory,
|
|
1231
|
-
skillSlug,
|
|
1232
|
-
ignored,
|
|
1233
|
-
preferenceScope
|
|
1234
|
-
}) => {
|
|
1235
|
-
const config = await resolveConfig(worktree);
|
|
1236
|
-
const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
|
|
1237
|
-
if (!normalizedSkillSlug) {
|
|
1238
|
-
throw new Error('Cannot toggle an empty published skill slug.');
|
|
1239
|
-
}
|
|
1240
|
-
return setPublishedSkillPreference({
|
|
1241
|
-
worktree,
|
|
1242
|
-
directory,
|
|
1243
|
-
config,
|
|
1244
|
-
skillSlug: normalizedSkillSlug,
|
|
1245
|
-
preferenceScope: preferenceScope ?? 'project',
|
|
1246
|
-
ignored
|
|
1247
|
-
});
|
|
1248
|
-
};
|
|
1249
|
-
export const setPublishedSkillInstalled = async ({
|
|
1250
|
-
worktree,
|
|
1251
|
-
directory,
|
|
1252
|
-
skillSlug,
|
|
1253
|
-
installed,
|
|
1254
|
-
preferenceScope
|
|
1255
|
-
}) => {
|
|
1256
|
-
const config = await resolveConfig(worktree);
|
|
1257
|
-
const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
|
|
1258
|
-
if (!normalizedSkillSlug) {
|
|
1259
|
-
throw new Error('Cannot toggle an empty published skill slug.');
|
|
1260
|
-
}
|
|
1261
|
-
return setPublishedSkillPreference({
|
|
1262
|
-
worktree,
|
|
1263
|
-
directory,
|
|
1264
|
-
config,
|
|
1265
|
-
skillSlug: normalizedSkillSlug,
|
|
1266
|
-
preferenceScope,
|
|
1267
|
-
installed
|
|
1268
|
-
});
|
|
1269
|
-
};
|
|
1270
|
-
const toPluginStatusMetadata = snapshot => ({
|
|
1271
|
-
backendOrigin: snapshot.backendOrigin,
|
|
1272
|
-
graphqlUrl: snapshot.graphqlUrl,
|
|
1273
|
-
pluginStatus: snapshot.status,
|
|
1274
|
-
authStatus: snapshot.authState.status,
|
|
1275
|
-
authEmail: snapshot.authState.email ?? '',
|
|
1276
|
-
authUserId: snapshot.authState.userId ?? '',
|
|
1277
|
-
directoryPath: snapshot.workspaceResolution.directoryPath,
|
|
1278
|
-
repositoryUrl: snapshot.workspaceResolution.repositoryUrl ?? '',
|
|
1279
|
-
source: snapshot.source,
|
|
1280
|
-
workspaceSlug: snapshot.workspaceResolution.workspaceSlug ?? '',
|
|
1281
|
-
workspaceSlugSource: snapshot.workspaceResolution.workspaceSlugSource ?? 'placeholder'
|
|
1282
|
-
});
|
|
1283
|
-
const isUnauthorizedGraphQlMessage = message => {
|
|
1284
|
-
const normalizedMessage = message.toLowerCase();
|
|
1285
|
-
return normalizedMessage.includes('not authorized') || normalizedMessage.includes('unauthorized');
|
|
1286
|
-
};
|
|
1287
|
-
const createRandomBase64Url = bytes => {
|
|
1288
|
-
return crypto.randomBytes(bytes).toString('base64url');
|
|
1289
|
-
};
|
|
1290
|
-
const createCodeChallenge = codeVerifier => {
|
|
1291
|
-
return crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
1292
|
-
};
|
|
1293
|
-
const getMessageFromUnknownPayload = value => {
|
|
1294
|
-
if (!value || typeof value !== 'object') return null;
|
|
1295
|
-
const candidate = 'message' in value ? value.message : null;
|
|
1296
|
-
return typeof candidate === 'string' ? candidate : null;
|
|
1297
|
-
};
|
|
1298
|
-
const wait = async milliseconds => {
|
|
1299
|
-
await new Promise(resolve => {
|
|
1300
|
-
setTimeout(resolve, milliseconds);
|
|
1301
|
-
});
|
|
1302
|
-
};
|
|
1303
|
-
const shouldRetryPresenceEvent = status => {
|
|
1304
|
-
return status === 408 || status === 429 || status >= 500;
|
|
1305
|
-
};
|
|
1306
|
-
const fetchOidcDiscoveryDocument = async signal => {
|
|
1307
|
-
const discoveryUrl = `${OIDC_ISSUER.replace(/\/+$/, '')}/.well-known/openid-configuration`;
|
|
1308
|
-
const response = await fetch(discoveryUrl, {
|
|
1309
|
-
method: 'GET',
|
|
1310
|
-
signal
|
|
1311
|
-
});
|
|
1312
|
-
if (!response.ok) {
|
|
1313
|
-
throw new Error(`OIDC discovery failed with HTTP ${response.status}.`);
|
|
1314
|
-
}
|
|
1315
|
-
return await response.json();
|
|
1316
|
-
};
|
|
1317
|
-
const isCallbackPortInUseError = error => {
|
|
1318
|
-
if (!error || typeof error !== 'object') return false;
|
|
1319
|
-
if (!('code' in error)) return false;
|
|
1320
|
-
return error.code === 'EADDRINUSE';
|
|
1321
|
-
};
|
|
1322
|
-
const toCallbackServerStartError = error => {
|
|
1323
|
-
if (!isCallbackPortInUseError(error)) {
|
|
1324
|
-
return error instanceof Error ? error : new Error('Failed to start local OAuth callback server.');
|
|
1325
|
-
}
|
|
1326
|
-
return new Error('OAuth login cannot start because localhost:24953 is already in use. Another OpenCode login is likely in progress; finish it or close the other instance, then retry.');
|
|
1327
|
-
};
|
|
1328
|
-
const startLocalCallbackServer = async ({
|
|
1329
|
-
expectedState,
|
|
1330
|
-
signal
|
|
1331
|
-
}) => {
|
|
1332
|
-
let settled = false;
|
|
1333
|
-
let resolvePayload = () => undefined;
|
|
1334
|
-
let rejectPayload = () => undefined;
|
|
1335
|
-
const callbackPromise = new Promise((resolve, reject) => {
|
|
1336
|
-
resolvePayload = resolve;
|
|
1337
|
-
rejectPayload = reject;
|
|
1338
|
-
});
|
|
1339
|
-
const finalize = payload => {
|
|
1340
|
-
if (settled) return;
|
|
1341
|
-
settled = true;
|
|
1342
|
-
resolvePayload(payload);
|
|
1343
|
-
};
|
|
1344
|
-
const fail = reason => {
|
|
1345
|
-
if (settled) return;
|
|
1346
|
-
settled = true;
|
|
1347
|
-
rejectPayload(reason);
|
|
1348
|
-
};
|
|
1349
|
-
const server = http.createServer((request, response) => {
|
|
1350
|
-
const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
|
|
1351
|
-
if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
|
|
1352
|
-
sendOAuthCallbackHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
const error = requestUrl.searchParams.get('error');
|
|
1356
|
-
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
1357
|
-
if (error) {
|
|
1358
|
-
const message = errorDescription ?? error;
|
|
1359
|
-
sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
|
|
1360
|
-
finalize({
|
|
1361
|
-
status: 'error',
|
|
1362
|
-
message
|
|
1363
|
-
});
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
const state = requestUrl.searchParams.get('state');
|
|
1367
|
-
const code = requestUrl.searchParams.get('code');
|
|
1368
|
-
if (!state || state !== expectedState) {
|
|
1369
|
-
sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
|
|
1370
|
-
finalize({
|
|
1371
|
-
status: 'error',
|
|
1372
|
-
message: 'OAuth state did not match the login request.'
|
|
1373
|
-
});
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
if (!code) {
|
|
1377
|
-
sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
|
|
1378
|
-
finalize({
|
|
1379
|
-
status: 'error',
|
|
1380
|
-
message: 'OAuth callback did not include an authorization code.'
|
|
1381
|
-
});
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
sendOAuthCallbackHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
|
|
1385
|
-
finalize({
|
|
1386
|
-
status: 'success',
|
|
1387
|
-
code,
|
|
1388
|
-
state
|
|
1389
|
-
});
|
|
1390
|
-
});
|
|
1391
|
-
const close = async () => {
|
|
1392
|
-
await new Promise((resolve, reject) => {
|
|
1393
|
-
server.close(error => {
|
|
1394
|
-
if (error) {
|
|
1395
|
-
reject(error);
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
resolve();
|
|
1399
|
-
});
|
|
1400
|
-
});
|
|
1401
|
-
};
|
|
1402
|
-
await new Promise((resolve, reject) => {
|
|
1403
|
-
const rejectStart = error => {
|
|
1404
|
-
reject(toCallbackServerStartError(error));
|
|
1405
|
-
};
|
|
1406
|
-
server.once('error', rejectStart);
|
|
1407
|
-
server.listen(24953, 'localhost', () => {
|
|
1408
|
-
server.off('error', rejectStart);
|
|
1409
|
-
server.on('error', error => {
|
|
1410
|
-
fail(error instanceof Error ? error : new Error('Local OAuth callback server failed.'));
|
|
1411
|
-
});
|
|
1412
|
-
resolve();
|
|
1413
|
-
});
|
|
1414
|
-
});
|
|
1415
|
-
signal.addEventListener('abort', () => {
|
|
1416
|
-
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
1417
|
-
void close().catch(() => undefined);
|
|
1418
|
-
}, {
|
|
1419
|
-
once: true
|
|
1420
|
-
});
|
|
1421
|
-
return {
|
|
1422
|
-
callbackPromise,
|
|
1423
|
-
close
|
|
1424
|
-
};
|
|
1425
|
-
};
|
|
1426
|
-
const fetchPublishedSkillsGraphQl = async ({
|
|
1427
|
-
worktree,
|
|
1428
|
-
config,
|
|
1429
|
-
query,
|
|
1430
|
-
variables,
|
|
1431
|
-
signal,
|
|
1432
|
-
onAuthStateChanged
|
|
1433
|
-
}) => {
|
|
1434
|
-
const authState = await resolveStoredAuthState(worktree, config);
|
|
1435
|
-
const fetchedAt = new Date().toISOString();
|
|
1436
|
-
if (!authState) {
|
|
1437
|
-
return {
|
|
1438
|
-
ok: false,
|
|
1439
|
-
result: {
|
|
1440
|
-
ok: false,
|
|
1441
|
-
status: 'missing_auth',
|
|
1442
|
-
authMode: 'missing',
|
|
1443
|
-
message: 'No plugin session is stored. Interactive opencode_wizard_published_skills_fetch can bootstrap browser login automatically when needed. Configured backend and GraphQL URLs are shown for visibility, but no GraphQL request was made because auth is missing.',
|
|
1444
|
-
fetchedAt,
|
|
1445
|
-
source: 'network'
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
let response;
|
|
1450
|
-
try {
|
|
1451
|
-
response = await fetch(config.graphqlUrl, {
|
|
1452
|
-
method: 'POST',
|
|
1453
|
-
headers: {
|
|
1454
|
-
'content-type': 'application/json',
|
|
1455
|
-
authorization: `Bearer ${authState.sessionToken}`
|
|
1456
|
-
},
|
|
1457
|
-
body: JSON.stringify({
|
|
1458
|
-
query,
|
|
1459
|
-
variables
|
|
1460
|
-
}),
|
|
1461
|
-
signal
|
|
1462
|
-
});
|
|
1463
|
-
} catch (error) {
|
|
1464
|
-
return {
|
|
1465
|
-
ok: false,
|
|
1466
|
-
result: {
|
|
1467
|
-
ok: false,
|
|
1468
|
-
status: 'request_failed',
|
|
1469
|
-
authMode: 'session',
|
|
1470
|
-
message: error instanceof Error ? error.message : 'Unknown fetch error',
|
|
1471
|
-
fetchedAt,
|
|
1472
|
-
source: 'network'
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
if (response.status === 401 || response.status === 403) {
|
|
1477
|
-
await clearAuthState(config.authStatePath);
|
|
1478
|
-
onAuthStateChanged?.();
|
|
1479
|
-
return {
|
|
1480
|
-
ok: false,
|
|
1481
|
-
result: {
|
|
1482
|
-
ok: false,
|
|
1483
|
-
status: 'missing_auth',
|
|
1484
|
-
authMode: 'session',
|
|
1485
|
-
message: 'Stored plugin session was rejected by the backend. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
1486
|
-
fetchedAt,
|
|
1487
|
-
source: 'network'
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
if (!response.ok) {
|
|
1492
|
-
return {
|
|
1493
|
-
ok: false,
|
|
1494
|
-
result: {
|
|
1495
|
-
ok: false,
|
|
1496
|
-
status: 'request_failed',
|
|
1497
|
-
authMode: 'session',
|
|
1498
|
-
message: `GraphQL request failed with HTTP ${response.status}.`,
|
|
1499
|
-
fetchedAt,
|
|
1500
|
-
source: 'network'
|
|
1501
|
-
}
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
let body;
|
|
1505
|
-
try {
|
|
1506
|
-
body = await response.json();
|
|
1507
|
-
} catch (error) {
|
|
1508
|
-
return {
|
|
1509
|
-
ok: false,
|
|
1510
|
-
result: {
|
|
1511
|
-
ok: false,
|
|
1512
|
-
status: 'request_failed',
|
|
1513
|
-
authMode: 'session',
|
|
1514
|
-
message: `GraphQL response was not valid JSON: ${error instanceof Error ? error.message : 'Unknown parse error'}`,
|
|
1515
|
-
fetchedAt,
|
|
1516
|
-
source: 'network'
|
|
1517
|
-
}
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
if (body.errors?.length) {
|
|
1521
|
-
const message = body.errors.map(error => error.message).join('; ');
|
|
1522
|
-
if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
|
|
1523
|
-
await clearAuthState(config.authStatePath);
|
|
1524
|
-
onAuthStateChanged?.();
|
|
1525
|
-
return {
|
|
1526
|
-
ok: false,
|
|
1527
|
-
result: {
|
|
1528
|
-
ok: false,
|
|
1529
|
-
status: 'missing_auth',
|
|
1530
|
-
authMode: 'session',
|
|
1531
|
-
message: 'Stored plugin session is no longer valid. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
|
|
1532
|
-
fetchedAt,
|
|
1533
|
-
source: 'network'
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
return {
|
|
1538
|
-
ok: false,
|
|
1539
|
-
result: {
|
|
1540
|
-
ok: false,
|
|
1541
|
-
status: 'request_failed',
|
|
1542
|
-
authMode: 'session',
|
|
1543
|
-
message,
|
|
1544
|
-
fetchedAt,
|
|
1545
|
-
source: 'network'
|
|
1546
|
-
}
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
if (!body.data) {
|
|
1550
|
-
return {
|
|
1551
|
-
ok: false,
|
|
1552
|
-
result: {
|
|
1553
|
-
ok: false,
|
|
1554
|
-
status: 'request_failed',
|
|
1555
|
-
authMode: 'session',
|
|
1556
|
-
message: 'GraphQL response did not include data.',
|
|
1557
|
-
fetchedAt,
|
|
1558
|
-
source: 'network'
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
return {
|
|
1563
|
-
ok: true,
|
|
1564
|
-
data: body.data,
|
|
1565
|
-
fetchedAt
|
|
1566
|
-
};
|
|
1567
|
-
};
|
|
1568
|
-
const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal, onAuthStateChanged) => {
|
|
1569
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
1570
|
-
worktree,
|
|
1571
|
-
config,
|
|
1572
|
-
query: PUBLISHED_SKILLS_CATALOG_QUERY,
|
|
1573
|
-
variables: {
|
|
1574
|
-
input: toDeliveryInput(resolution)
|
|
1575
|
-
},
|
|
1576
|
-
signal,
|
|
1577
|
-
onAuthStateChanged
|
|
1578
|
-
});
|
|
1579
|
-
if (!response.ok) {
|
|
1580
|
-
return response.result;
|
|
1581
|
-
}
|
|
1582
|
-
const payload = response.data.pluginPublishedSkills;
|
|
1583
|
-
if (!payload) {
|
|
1584
|
-
return {
|
|
1585
|
-
ok: false,
|
|
1586
|
-
status: 'request_failed',
|
|
1587
|
-
authMode: 'session',
|
|
1588
|
-
message: 'GraphQL response did not include pluginPublishedSkills.',
|
|
1589
|
-
fetchedAt: response.fetchedAt,
|
|
1590
|
-
source: 'network'
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
return {
|
|
1594
|
-
ok: true,
|
|
1595
|
-
status: 'ready',
|
|
1596
|
-
authMode: 'session',
|
|
1597
|
-
payload,
|
|
1598
|
-
fetchedAt: response.fetchedAt,
|
|
1599
|
-
source: 'network'
|
|
1600
|
-
};
|
|
1601
|
-
};
|
|
1602
|
-
const maybePersistWorkspaceSlugFromCatalog = async ({
|
|
1603
|
-
config,
|
|
1604
|
-
resolution,
|
|
1605
|
-
fetchResult
|
|
1606
|
-
}) => {
|
|
1607
|
-
if (!fetchResult.ok) return;
|
|
1608
|
-
const backendWorkspaceSlug = fetchResult.payload.workspace?.slug?.trim();
|
|
1609
|
-
if (!backendWorkspaceSlug) return;
|
|
1610
|
-
const normalizedWorkspaceSlug = toWorkspaceSlug(backendWorkspaceSlug);
|
|
1611
|
-
if (!normalizedWorkspaceSlug) return;
|
|
1612
|
-
if (resolution.workspaceSlug === normalizedWorkspaceSlug && resolution.workspaceSlugSource !== 'fallback') return;
|
|
1613
|
-
await upsertWorkspaceSlugMapping({
|
|
1614
|
-
configFile: config.authStatePath,
|
|
1615
|
-
repositoryUrl: resolution.repositoryUrl,
|
|
1616
|
-
repositoryRoot: resolution.repositoryRoot,
|
|
1617
|
-
workspaceSlug: normalizedWorkspaceSlug
|
|
1618
|
-
});
|
|
1619
|
-
};
|
|
1620
|
-
const fetchPublishedSkillDetail = async ({
|
|
1621
|
-
worktree,
|
|
1622
|
-
config,
|
|
1623
|
-
resolution,
|
|
1624
|
-
skillVersionId,
|
|
1625
|
-
signal,
|
|
1626
|
-
onAuthStateChanged,
|
|
1627
|
-
purpose
|
|
1628
|
-
}) => {
|
|
1629
|
-
const response = await fetchPublishedSkillsGraphQl({
|
|
1630
|
-
worktree,
|
|
1631
|
-
config,
|
|
1632
|
-
query: PUBLISHED_SKILL_DETAIL_QUERY,
|
|
1633
|
-
variables: {
|
|
1634
|
-
input: {
|
|
1635
|
-
...toDeliveryInput(resolution),
|
|
1636
|
-
skillVersionId,
|
|
1637
|
-
purpose
|
|
1638
|
-
}
|
|
1639
|
-
},
|
|
1640
|
-
signal,
|
|
1641
|
-
onAuthStateChanged
|
|
1642
|
-
});
|
|
1643
|
-
if (!response.ok) {
|
|
1644
|
-
return response;
|
|
1645
|
-
}
|
|
1646
|
-
const artifact = response.data.pluginPublishedSkillVersionArtifact;
|
|
1647
|
-
if (!artifact) {
|
|
1648
|
-
return {
|
|
1649
|
-
ok: false,
|
|
1650
|
-
result: {
|
|
1651
|
-
ok: false,
|
|
1652
|
-
status: 'not_found',
|
|
1653
|
-
authMode: 'session',
|
|
1654
|
-
message: 'Published skill detail is not effective for the current scope.',
|
|
1655
|
-
fetchedAt: response.fetchedAt,
|
|
1656
|
-
source: 'network'
|
|
1657
|
-
}
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
return {
|
|
1661
|
-
ok: true,
|
|
1662
|
-
artifact
|
|
1663
|
-
};
|
|
1664
|
-
};
|
|
1665
|
-
const toFetchFailureOutput = async ({
|
|
1666
|
-
worktree,
|
|
1667
|
-
config,
|
|
1668
|
-
publishedSkillsResult,
|
|
1669
|
-
loginBootstrapSnapshot
|
|
1670
|
-
}) => ({
|
|
1671
|
-
output: await formatStatusOutput(worktree, config, publishedSkillsResult, loginBootstrapSnapshot),
|
|
1672
|
-
metadata: {
|
|
1673
|
-
status: publishedSkillsResult.fetchResult.status,
|
|
1674
|
-
...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
|
|
1675
|
-
source: publishedSkillsResult.fetchResult.source
|
|
1676
|
-
}
|
|
1677
|
-
});
|
|
1678
|
-
const startLoginFlow = async signal => {
|
|
1679
|
-
const discovery = await fetchOidcDiscoveryDocument(signal);
|
|
1680
|
-
const codeVerifier = createRandomBase64Url(64);
|
|
1681
|
-
const expectedState = createRandomBase64Url(32);
|
|
1682
|
-
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
1683
|
-
const expiresAt = new Date(Date.now() + LOGIN_TIMEOUT_MS).toISOString();
|
|
1684
|
-
const {
|
|
1685
|
-
callbackPromise,
|
|
1686
|
-
close
|
|
1687
|
-
} = await startLocalCallbackServer({
|
|
1688
|
-
expectedState,
|
|
1689
|
-
signal
|
|
1690
|
-
});
|
|
1691
|
-
const browserUrl = new URL(discovery.authorization_endpoint);
|
|
1692
|
-
browserUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
|
|
1693
|
-
browserUrl.searchParams.set('response_type', 'code');
|
|
1694
|
-
browserUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
|
|
1695
|
-
browserUrl.searchParams.set('response_mode', 'query');
|
|
1696
|
-
browserUrl.searchParams.set('scope', OIDC_SCOPES.join(' '));
|
|
1697
|
-
browserUrl.searchParams.set('code_challenge', codeChallenge);
|
|
1698
|
-
browserUrl.searchParams.set('code_challenge_method', 'S256');
|
|
1699
|
-
browserUrl.searchParams.set('state', expectedState);
|
|
1700
|
-
return {
|
|
1701
|
-
browserUrl: browserUrl.toString(),
|
|
1702
|
-
expiresAt,
|
|
1703
|
-
codeVerifier,
|
|
1704
|
-
expectedState,
|
|
1705
|
-
callbackPromise,
|
|
1706
|
-
closeCallbackServer: close
|
|
1707
|
-
};
|
|
1708
|
-
};
|
|
1709
|
-
const createPluginSession = async ({
|
|
1710
|
-
code,
|
|
1711
|
-
codeVerifier,
|
|
1712
|
-
redirectUri,
|
|
1713
|
-
config,
|
|
1714
|
-
signal
|
|
1715
|
-
}) => {
|
|
1716
|
-
const response = await fetch(config.authSessionUrl, {
|
|
1717
|
-
method: 'POST',
|
|
1718
|
-
headers: {
|
|
1719
|
-
'content-type': 'application/json'
|
|
1720
|
-
},
|
|
1721
|
-
body: JSON.stringify({
|
|
1722
|
-
code,
|
|
1723
|
-
codeVerifier,
|
|
1724
|
-
redirectUri
|
|
1725
|
-
}),
|
|
1726
|
-
signal
|
|
1727
|
-
});
|
|
1728
|
-
const payload = await response.json().catch(() => null);
|
|
1729
|
-
if (!response.ok) {
|
|
1730
|
-
throw new Error(getMessageFromUnknownPayload(payload) ?? `Plugin session exchange failed with HTTP ${response.status}.`);
|
|
1731
|
-
}
|
|
1732
|
-
if (!payload || !('success' in payload) || payload.success !== true) {
|
|
1733
|
-
throw new Error('Plugin session exchange returned an unexpected payload.');
|
|
1734
|
-
}
|
|
1735
|
-
return payload.session;
|
|
1736
|
-
};
|
|
1737
|
-
const emitPresenceEvent = async ({
|
|
1738
|
-
config,
|
|
1739
|
-
authState,
|
|
1740
|
-
event,
|
|
1741
|
-
workspacePath
|
|
1742
|
-
}) => {
|
|
1743
|
-
for (let attempt = 1; attempt <= PRESENCE_EVENT_MAX_ATTEMPTS; attempt += 1) {
|
|
1744
|
-
try {
|
|
1745
|
-
const response = await fetch(config.presenceUrl, {
|
|
1746
|
-
method: 'POST',
|
|
1747
|
-
headers: {
|
|
1748
|
-
'content-type': 'application/json',
|
|
1749
|
-
authorization: `Bearer ${authState.sessionToken}`
|
|
1750
|
-
},
|
|
1751
|
-
body: JSON.stringify({
|
|
1752
|
-
event,
|
|
1753
|
-
occurredAt: new Date().toISOString(),
|
|
1754
|
-
workspacePath
|
|
1755
|
-
}),
|
|
1756
|
-
keepalive: event === 'STOP',
|
|
1757
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1758
|
-
});
|
|
1759
|
-
if (response.ok) return;
|
|
1760
|
-
if (!shouldRetryPresenceEvent(response.status) || attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
|
|
1761
|
-
} catch {
|
|
1762
|
-
if (attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
|
|
1763
|
-
}
|
|
1764
|
-
await wait(PRESENCE_EVENT_RETRY_DELAY_MS * attempt);
|
|
1765
|
-
}
|
|
1766
|
-
};
|
|
1767
|
-
const emitPluginActionEvent = async ({
|
|
1768
|
-
config,
|
|
1769
|
-
authState,
|
|
1770
|
-
event,
|
|
1771
|
-
workspacePath,
|
|
1772
|
-
directoryPath
|
|
1773
|
-
}) => {
|
|
1774
|
-
if (!authState) return;
|
|
1775
|
-
try {
|
|
1776
|
-
await fetch(config.actionsUrl, {
|
|
1777
|
-
method: 'POST',
|
|
1778
|
-
headers: {
|
|
1779
|
-
'content-type': 'application/json',
|
|
1780
|
-
authorization: `Bearer ${authState.sessionToken}`
|
|
1781
|
-
},
|
|
1782
|
-
body: JSON.stringify({
|
|
1783
|
-
event,
|
|
1784
|
-
occurredAt: new Date().toISOString(),
|
|
1785
|
-
workspacePath,
|
|
1786
|
-
directoryPath
|
|
1787
|
-
}),
|
|
1788
|
-
keepalive: event === 'STOP',
|
|
1789
|
-
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1790
|
-
});
|
|
1791
|
-
} catch {
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
};
|
|
1795
|
-
const openBrowser = async url => {
|
|
1796
|
-
try {
|
|
1797
|
-
if (process.platform === 'darwin') {
|
|
1798
|
-
await execFileAsync('open', [url]);
|
|
1799
|
-
return null;
|
|
1800
|
-
}
|
|
1801
|
-
if (process.platform === 'win32') {
|
|
1802
|
-
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
1803
|
-
return null;
|
|
1804
|
-
}
|
|
1805
|
-
await execFileAsync('xdg-open', [url]);
|
|
1806
|
-
return null;
|
|
1807
|
-
} catch (error) {
|
|
1808
|
-
return error instanceof Error ? error.message : 'Failed to open browser automatically';
|
|
1809
|
-
}
|
|
1810
|
-
};
|
|
1811
|
-
const normalizeDirectoryArg = (contextDirectory, directory) => {
|
|
1812
|
-
return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
|
|
1813
|
-
};
|
|
1814
|
-
const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
|
|
1815
|
-
return JSON.stringify([catalogCacheKey, skillVersionId]);
|
|
1816
|
-
};
|
|
1817
|
-
const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
|
|
1818
|
-
return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
|
|
1819
|
-
};
|
|
1820
|
-
const OpencodeWizardSkillsPlugin = async input => {
|
|
1821
|
-
const {
|
|
1822
|
-
tool
|
|
1823
|
-
} = await importOpencodePluginModule('@opencode-ai/plugin');
|
|
1824
|
-
const config = await resolveConfig(input.worktree);
|
|
1825
|
-
const workspacePath = normalizeAbsolutePath(input.worktree);
|
|
1826
|
-
const cache = new Map();
|
|
1827
|
-
const catalogInflight = new Map();
|
|
1828
|
-
const detailCache = new Map();
|
|
1829
|
-
const detailInflight = new Map();
|
|
1830
|
-
const initialAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1831
|
-
const loginBootstrap = {
|
|
1832
|
-
promise: null,
|
|
1833
|
-
snapshot: createIdleLoginBootstrapSnapshot()
|
|
1834
|
-
};
|
|
1835
|
-
let lastAuthenticatedAuthState = initialAuthState;
|
|
1836
|
-
let didEmitStart = false;
|
|
1837
|
-
let didScheduleStop = false;
|
|
1838
|
-
let presenceStartPromise = null;
|
|
1839
|
-
let presenceStopPromise = null;
|
|
1840
|
-
let lastInteractiveDirectoryPath = null;
|
|
1841
|
-
const resolveActionAuthState = async () => {
|
|
1842
|
-
const storedAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1843
|
-
if (storedAuthState) return storedAuthState;
|
|
1844
|
-
return lastAuthenticatedAuthState;
|
|
1845
|
-
};
|
|
1846
|
-
const emitActionEventForCurrentSession = async ({
|
|
1847
|
-
event,
|
|
1848
|
-
authState,
|
|
1849
|
-
directoryPath
|
|
1850
|
-
}) => {
|
|
1851
|
-
await emitPluginActionEvent({
|
|
1852
|
-
config,
|
|
1853
|
-
authState: authState ?? (await resolveActionAuthState()),
|
|
1854
|
-
event,
|
|
1855
|
-
workspacePath,
|
|
1856
|
-
directoryPath
|
|
1857
|
-
});
|
|
1858
|
-
};
|
|
1859
|
-
const schedulePresenceStart = authState => {
|
|
1860
|
-
lastAuthenticatedAuthState = authState;
|
|
1861
|
-
if (didEmitStart) {
|
|
1862
|
-
return presenceStartPromise ?? Promise.resolve();
|
|
1863
|
-
}
|
|
1864
|
-
didEmitStart = true;
|
|
1865
|
-
presenceStartPromise = Promise.all([emitPresenceEvent({
|
|
1866
|
-
config,
|
|
1867
|
-
authState,
|
|
1868
|
-
event: 'START',
|
|
1869
|
-
workspacePath
|
|
1870
|
-
}), emitActionEventForCurrentSession({
|
|
1871
|
-
event: 'START',
|
|
1872
|
-
authState,
|
|
1873
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1874
|
-
})]).then(() => undefined);
|
|
1875
|
-
return presenceStartPromise;
|
|
1876
|
-
};
|
|
1877
|
-
const schedulePresenceStop = () => {
|
|
1878
|
-
if (didScheduleStop) {
|
|
1879
|
-
return presenceStopPromise ?? Promise.resolve();
|
|
1880
|
-
}
|
|
1881
|
-
didScheduleStop = true;
|
|
1882
|
-
if (!didEmitStart || !lastAuthenticatedAuthState) {
|
|
1883
|
-
presenceStopPromise = Promise.resolve();
|
|
1884
|
-
return presenceStopPromise;
|
|
1885
|
-
}
|
|
1886
|
-
presenceStopPromise = (async () => {
|
|
1887
|
-
await presenceStartPromise?.catch(() => undefined);
|
|
1888
|
-
await Promise.all([emitPresenceEvent({
|
|
1889
|
-
config,
|
|
1890
|
-
authState: lastAuthenticatedAuthState,
|
|
1891
|
-
event: 'STOP',
|
|
1892
|
-
workspacePath
|
|
1893
|
-
}), emitActionEventForCurrentSession({
|
|
1894
|
-
event: 'STOP',
|
|
1895
|
-
authState: lastAuthenticatedAuthState,
|
|
1896
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1897
|
-
})]);
|
|
1898
|
-
})();
|
|
1899
|
-
return presenceStopPromise;
|
|
1900
|
-
};
|
|
1901
|
-
const scheduleInteractivePresenceStart = async () => {
|
|
1902
|
-
const authState = await resolveStoredAuthState(input.worktree, config);
|
|
1903
|
-
if (!authState) return;
|
|
1904
|
-
await schedulePresenceStart(authState);
|
|
1905
|
-
};
|
|
1906
|
-
process.once('beforeExit', () => {
|
|
1907
|
-
void schedulePresenceStop();
|
|
1908
|
-
});
|
|
1909
|
-
for (const shutdownSignal of PRESENCE_SHUTDOWN_SIGNALS) {
|
|
1910
|
-
try {
|
|
1911
|
-
process.once(shutdownSignal, () => {
|
|
1912
|
-
void schedulePresenceStop().finally(() => {
|
|
1913
|
-
process.exit(PRESENCE_SIGNAL_EXIT_CODES[shutdownSignal]);
|
|
1914
|
-
});
|
|
1915
|
-
});
|
|
1916
|
-
} catch {
|
|
1917
|
-
continue;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
const clearPublishedSkillState = () => {
|
|
1921
|
-
cache.clear();
|
|
1922
|
-
catalogInflight.clear();
|
|
1923
|
-
detailCache.clear();
|
|
1924
|
-
detailInflight.clear();
|
|
1925
|
-
};
|
|
1926
|
-
const persistAuthState = async session => {
|
|
1927
|
-
const authState = toAuthState(session);
|
|
1928
|
-
await writeAuthState(config.authStatePath, authState);
|
|
1929
|
-
clearPublishedSkillState();
|
|
1930
|
-
return authState;
|
|
1931
|
-
};
|
|
1932
|
-
const startLoginCompletion = trigger => {
|
|
1933
|
-
if (loginBootstrap.promise) {
|
|
1934
|
-
return loginBootstrap.promise;
|
|
1935
|
-
}
|
|
1936
|
-
const startedAt = new Date().toISOString();
|
|
1937
|
-
loginBootstrap.snapshot = {
|
|
1938
|
-
status: 'starting',
|
|
1939
|
-
trigger,
|
|
1940
|
-
startedAt,
|
|
1941
|
-
expiresAt: null,
|
|
1942
|
-
browserUrl: null,
|
|
1943
|
-
browserOpenError: null,
|
|
1944
|
-
email: null,
|
|
1945
|
-
message: null
|
|
1946
|
-
};
|
|
1947
|
-
const loginPromise = (async () => {
|
|
1948
|
-
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1949
|
-
let loginStart = null;
|
|
1950
|
-
try {
|
|
1951
|
-
loginStart = await startLoginFlow(loginSignal);
|
|
1952
|
-
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1953
|
-
loginBootstrap.snapshot = {
|
|
1954
|
-
status: 'pending',
|
|
1955
|
-
trigger,
|
|
1956
|
-
startedAt,
|
|
1957
|
-
expiresAt: loginStart.expiresAt,
|
|
1958
|
-
browserUrl: loginStart.browserUrl,
|
|
1959
|
-
browserOpenError,
|
|
1960
|
-
email: null,
|
|
1961
|
-
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
|
|
1962
|
-
};
|
|
1963
|
-
const callbackPayload = await loginStart.callbackPromise;
|
|
1964
|
-
if (callbackPayload.status === 'error') {
|
|
1965
|
-
throw new Error(callbackPayload.message);
|
|
1966
|
-
}
|
|
1967
|
-
if (callbackPayload.state !== loginStart.expectedState) {
|
|
1968
|
-
throw new Error('OAuth callback state did not match the original login request.');
|
|
1969
|
-
}
|
|
1970
|
-
loginBootstrap.snapshot = {
|
|
1971
|
-
status: 'pending',
|
|
1972
|
-
trigger,
|
|
1973
|
-
startedAt,
|
|
1974
|
-
expiresAt: loginStart.expiresAt,
|
|
1975
|
-
browserUrl: loginStart.browserUrl,
|
|
1976
|
-
browserOpenError,
|
|
1977
|
-
email: null,
|
|
1978
|
-
message: 'OAuth callback received. Finalizing backend session exchange.'
|
|
1979
|
-
};
|
|
1980
|
-
const pluginSession = await createPluginSession({
|
|
1981
|
-
code: callbackPayload.code,
|
|
1982
|
-
codeVerifier: loginStart.codeVerifier,
|
|
1983
|
-
redirectUri: OIDC_CALLBACK_URL,
|
|
1984
|
-
config,
|
|
1985
|
-
signal: loginSignal
|
|
1986
|
-
});
|
|
1987
|
-
const authState = await persistAuthState(pluginSession);
|
|
1988
|
-
await emitActionEventForCurrentSession({
|
|
1989
|
-
event: 'LOGIN_SUCCESS',
|
|
1990
|
-
authState,
|
|
1991
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
1992
|
-
});
|
|
1993
|
-
loginBootstrap.snapshot = {
|
|
1994
|
-
status: 'authenticated',
|
|
1995
|
-
trigger,
|
|
1996
|
-
startedAt,
|
|
1997
|
-
expiresAt: authState.expiresAt,
|
|
1998
|
-
browserUrl: loginStart.browserUrl,
|
|
1999
|
-
browserOpenError,
|
|
2000
|
-
email: authState.email,
|
|
2001
|
-
message: `Browser login completed successfully for published skill ${trigger}.`
|
|
2002
|
-
};
|
|
2003
|
-
return authState;
|
|
2004
|
-
} catch (error) {
|
|
2005
|
-
await emitActionEventForCurrentSession({
|
|
2006
|
-
event: 'LOGIN_FAILED',
|
|
2007
|
-
directoryPath: lastInteractiveDirectoryPath ?? undefined
|
|
2008
|
-
});
|
|
2009
|
-
loginBootstrap.snapshot = {
|
|
2010
|
-
status: 'failed',
|
|
2011
|
-
trigger,
|
|
2012
|
-
startedAt,
|
|
2013
|
-
expiresAt: loginBootstrap.snapshot.expiresAt,
|
|
2014
|
-
browserUrl: loginBootstrap.snapshot.browserUrl,
|
|
2015
|
-
browserOpenError: loginBootstrap.snapshot.browserOpenError,
|
|
2016
|
-
email: null,
|
|
2017
|
-
message: error instanceof Error ? error.message : 'Browser login failed.'
|
|
2018
|
-
};
|
|
2019
|
-
throw error;
|
|
2020
|
-
} finally {
|
|
2021
|
-
await loginStart?.closeCallbackServer().catch(() => undefined);
|
|
2022
|
-
loginBootstrap.promise = null;
|
|
2023
|
-
}
|
|
2024
|
-
})();
|
|
2025
|
-
loginBootstrap.promise = loginPromise;
|
|
2026
|
-
return loginPromise;
|
|
2027
|
-
};
|
|
2028
|
-
const loadPublishedSkillCatalog = async ({
|
|
2029
|
-
directory,
|
|
2030
|
-
useCache,
|
|
2031
|
-
signal
|
|
2032
|
-
}) => {
|
|
2033
|
-
const workspaceResolution = await resolveWorkspace({
|
|
2034
|
-
config,
|
|
2035
|
-
directory
|
|
2036
|
-
});
|
|
2037
|
-
const directoryPath = workspaceResolution.directoryPath;
|
|
2038
|
-
const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
|
|
2039
|
-
const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
|
|
2040
|
-
const cached = cache.get(cacheKey);
|
|
2041
|
-
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
2042
|
-
return {
|
|
2043
|
-
directoryPath,
|
|
2044
|
-
workspaceResolution,
|
|
2045
|
-
fetchResult: {
|
|
2046
|
-
...cached.result,
|
|
2047
|
-
source: 'cache'
|
|
2048
|
-
}
|
|
2049
|
-
};
|
|
2050
|
-
}
|
|
2051
|
-
const inflight = catalogInflight.get(cacheKey);
|
|
2052
|
-
if (inflight) {
|
|
2053
|
-
return inflight;
|
|
2054
|
-
}
|
|
2055
|
-
const requestPromise = (async () => {
|
|
2056
|
-
const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
|
|
2057
|
-
await maybePersistWorkspaceSlugFromCatalog({
|
|
2058
|
-
config,
|
|
2059
|
-
resolution: workspaceResolution,
|
|
2060
|
-
fetchResult
|
|
2061
|
-
});
|
|
2062
|
-
cache.set(cacheKey, {
|
|
2063
|
-
result: fetchResult,
|
|
2064
|
-
expiresAt: Date.now() + CACHE_TTL_MS
|
|
2065
|
-
});
|
|
2066
|
-
return {
|
|
2067
|
-
directoryPath,
|
|
2068
|
-
workspaceResolution,
|
|
2069
|
-
fetchResult
|
|
2070
|
-
};
|
|
2071
|
-
})();
|
|
2072
|
-
catalogInflight.set(cacheKey, requestPromise);
|
|
2073
|
-
try {
|
|
2074
|
-
return await requestPromise;
|
|
2075
|
-
} finally {
|
|
2076
|
-
catalogInflight.delete(cacheKey);
|
|
2077
|
-
}
|
|
2078
|
-
};
|
|
2079
|
-
const loadPublishedSkillDetail = async ({
|
|
2080
|
-
workspaceResolution,
|
|
2081
|
-
item,
|
|
2082
|
-
signal,
|
|
2083
|
-
useCache,
|
|
2084
|
-
purpose
|
|
2085
|
-
}) => {
|
|
2086
|
-
const directoryPath = workspaceResolution.directoryPath;
|
|
2087
|
-
const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
|
|
2088
|
-
const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
|
|
2089
|
-
const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
|
|
2090
|
-
const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
|
|
2091
|
-
const cached = detailCache.get(cacheKey);
|
|
2092
|
-
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
2093
|
-
return {
|
|
2094
|
-
ok: true,
|
|
2095
|
-
detail: toPublishedSkillDetail({
|
|
2096
|
-
...item,
|
|
2097
|
-
publishedArtifact: cached.artifact
|
|
2098
|
-
})
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
const inflight = detailInflight.get(inflightKey);
|
|
2102
|
-
if (inflight) {
|
|
2103
|
-
return inflight;
|
|
2104
|
-
}
|
|
2105
|
-
const requestPromise = (async () => {
|
|
2106
|
-
const detailResult = await fetchPublishedSkillDetail({
|
|
2107
|
-
worktree: input.worktree,
|
|
2108
|
-
config,
|
|
2109
|
-
resolution: workspaceResolution,
|
|
2110
|
-
skillVersionId: item.skillVersion.id,
|
|
2111
|
-
signal,
|
|
2112
|
-
onAuthStateChanged: clearPublishedSkillState,
|
|
2113
|
-
purpose
|
|
2114
|
-
});
|
|
2115
|
-
if (!detailResult.ok) {
|
|
2116
|
-
return {
|
|
2117
|
-
ok: false,
|
|
2118
|
-
status: detailResult.result.status,
|
|
2119
|
-
output: JSON.stringify({
|
|
2120
|
-
pluginId: PLUGIN_ID,
|
|
2121
|
-
runtimeMode: 'tool_fetch_only',
|
|
2122
|
-
status: detailResult.result.status,
|
|
2123
|
-
requestedDirectoryPath: directoryPath,
|
|
2124
|
-
workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
|
|
2125
|
-
requestedSkillVersionId: item.skillVersion.id,
|
|
2126
|
-
message: detailResult.result.message,
|
|
2127
|
-
fetchedAt: detailResult.result.fetchedAt,
|
|
2128
|
-
source: detailResult.result.source
|
|
2129
|
-
}, null, 2),
|
|
2130
|
-
metadata: {
|
|
2131
|
-
status: detailResult.result.status,
|
|
2132
|
-
...toWorkspaceResolutionMetadata(workspaceResolution),
|
|
2133
|
-
source: detailResult.result.source
|
|
2134
|
-
}
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
detailCache.set(cacheKey, {
|
|
2138
|
-
artifact: detailResult.artifact,
|
|
2139
|
-
expiresAt: Date.now() + CACHE_TTL_MS
|
|
2140
|
-
});
|
|
2141
|
-
return {
|
|
2142
|
-
ok: true,
|
|
2143
|
-
detail: toPublishedSkillDetail({
|
|
2144
|
-
...item,
|
|
2145
|
-
publishedArtifact: detailResult.artifact
|
|
2146
|
-
})
|
|
2147
|
-
};
|
|
2148
|
-
})();
|
|
2149
|
-
detailInflight.set(inflightKey, requestPromise);
|
|
2150
|
-
try {
|
|
2151
|
-
return await requestPromise;
|
|
2152
|
-
} finally {
|
|
2153
|
-
detailInflight.delete(inflightKey);
|
|
2154
|
-
}
|
|
2155
|
-
};
|
|
2156
|
-
const loadSystemNoteDetails = async ({
|
|
2157
|
-
publishedSkillsResult,
|
|
2158
|
-
signal
|
|
2159
|
-
}) => {
|
|
2160
|
-
if (!publishedSkillsResult.fetchResult.ok) return [];
|
|
2161
|
-
const prioritizedItems = [...publishedSkillsResult.fetchResult.payload.skills].sort((left, right) => {
|
|
2162
|
-
const leftSummary = toPublishedSkillSummary(left);
|
|
2163
|
-
const rightSummary = toPublishedSkillSummary(right);
|
|
2164
|
-
if (leftSummary.contextKind === rightSummary.contextKind) return formatSkillLabel(left).localeCompare(formatSkillLabel(right));
|
|
2165
|
-
if (leftSummary.contextKind === 'global') return -1;
|
|
2166
|
-
if (rightSummary.contextKind === 'global') return 1;
|
|
2167
|
-
return 0;
|
|
2168
|
-
});
|
|
2169
|
-
const detailResults = await Promise.all(prioritizedItems.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(item => loadPublishedSkillDetail({
|
|
2170
|
-
workspaceResolution: publishedSkillsResult.workspaceResolution,
|
|
2171
|
-
item,
|
|
2172
|
-
signal,
|
|
2173
|
-
useCache: true,
|
|
2174
|
-
purpose: 'SYSTEM_CONTEXT'
|
|
2175
|
-
})));
|
|
2176
|
-
return detailResults.reduce((details, result) => {
|
|
2177
|
-
if (!result.ok) return details;
|
|
2178
|
-
details.push(result.detail);
|
|
2179
|
-
return details;
|
|
2180
|
-
}, []);
|
|
2181
|
-
};
|
|
2182
|
-
const executePublishedSkillsFetchTool = async ({
|
|
2183
|
-
args,
|
|
2184
|
-
context
|
|
2185
|
-
}) => {
|
|
2186
|
-
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
2187
|
-
const requestedSkills = parseRequestedSkillArgs(args);
|
|
2188
|
-
const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
|
|
2189
|
-
lastInteractiveDirectoryPath = fetchActionDirectoryPath;
|
|
2190
|
-
const emitFetchOutcome = async event => {
|
|
2191
|
-
await emitActionEventForCurrentSession({
|
|
2192
|
-
event,
|
|
2193
|
-
directoryPath: fetchActionDirectoryPath
|
|
2194
|
-
});
|
|
2195
|
-
};
|
|
2196
|
-
let publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
2197
|
-
directory: requestedDirectory,
|
|
2198
|
-
useCache: !args.refresh,
|
|
2199
|
-
signal: context.abort
|
|
2200
|
-
});
|
|
2201
|
-
if (publishedSkillsResult.fetchResult.ok) {
|
|
2202
|
-
await scheduleInteractivePresenceStart();
|
|
2203
|
-
}
|
|
2204
|
-
if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
|
|
2205
|
-
try {
|
|
2206
|
-
await startLoginCompletion('fetch').then(async authState => {
|
|
2207
|
-
await schedulePresenceStart(authState);
|
|
2208
|
-
});
|
|
2209
|
-
publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
2210
|
-
directory: requestedDirectory,
|
|
2211
|
-
useCache: false,
|
|
2212
|
-
signal: context.abort
|
|
2213
|
-
});
|
|
2214
|
-
if (publishedSkillsResult.fetchResult.ok) {
|
|
2215
|
-
await scheduleInteractivePresenceStart();
|
|
2216
|
-
}
|
|
2217
|
-
} catch {
|
|
2218
|
-
// Return the original fetch failure with the latest login bootstrap snapshot attached.
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
if (!publishedSkillsResult.fetchResult.ok) {
|
|
2222
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
2223
|
-
return toFetchFailureOutput({
|
|
2224
|
-
worktree: input.worktree,
|
|
2225
|
-
config,
|
|
2226
|
-
publishedSkillsResult,
|
|
2227
|
-
loginBootstrapSnapshot: loginBootstrap.snapshot
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
2231
|
-
if (!filteredPublishedSkillsResult.fetchResult.ok) {
|
|
2232
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
2233
|
-
return toFetchFailureOutput({
|
|
2234
|
-
worktree: input.worktree,
|
|
2235
|
-
config,
|
|
2236
|
-
publishedSkillsResult: filteredPublishedSkillsResult,
|
|
2237
|
-
loginBootstrapSnapshot: loginBootstrap.snapshot
|
|
2238
|
-
});
|
|
2239
|
-
}
|
|
2240
|
-
const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
|
|
2241
|
-
const isSingleRequest = requestedSkills.length === 1;
|
|
2242
|
-
if (requestedSkills.length === 0) {
|
|
2243
|
-
const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
|
|
2244
|
-
context.metadata({
|
|
2245
|
-
title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
|
|
2246
|
-
metadata: {
|
|
2247
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2248
|
-
status: 'ready',
|
|
2249
|
-
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
2250
|
-
globalAssignmentCount: catalog.assignmentCounts.global.toString(),
|
|
2251
|
-
projectAssignmentCount: catalog.assignmentCounts.project.toString(),
|
|
2252
|
-
userAssignmentCount: catalog.assignmentCounts.user.toString(),
|
|
2253
|
-
ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
|
|
2254
|
-
}
|
|
2255
|
-
});
|
|
2256
|
-
await emitFetchOutcome('FETCH_SUCCESS');
|
|
2257
|
-
return {
|
|
2258
|
-
output: JSON.stringify({
|
|
2259
|
-
...catalog,
|
|
2260
|
-
status: 'ready',
|
|
2261
|
-
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2262
|
-
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2263
|
-
ignoredPublishedSkills: {
|
|
2264
|
-
scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
|
|
2265
|
-
count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
|
|
2266
|
-
},
|
|
2267
|
-
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2268
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2269
|
-
cacheTtlMs: CACHE_TTL_MS,
|
|
2270
|
-
message: args.refresh ? 'Catalog discovery refreshed from the backend. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.' : 'Catalog discovery only. Cached results expire automatically after 30 seconds, or pass `refresh: true` to force a backend refresh immediately. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.'
|
|
2271
|
-
}, null, 2),
|
|
2272
|
-
metadata: {
|
|
2273
|
-
status: 'ready',
|
|
2274
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2275
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2276
|
-
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
2277
|
-
globalAssignmentCount: catalog.assignmentCounts.global.toString(),
|
|
2278
|
-
projectAssignmentCount: catalog.assignmentCounts.project.toString(),
|
|
2279
|
-
userAssignmentCount: catalog.assignmentCounts.user.toString(),
|
|
2280
|
-
ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
|
|
2281
|
-
}
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
if (selection.selectedItems.length === 0 && isSingleRequest) {
|
|
2285
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
2286
|
-
return {
|
|
2287
|
-
output: JSON.stringify({
|
|
2288
|
-
pluginId: PLUGIN_ID,
|
|
2289
|
-
runtimeMode: 'tool_fetch_only',
|
|
2290
|
-
status: 'not_found',
|
|
2291
|
-
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2292
|
-
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2293
|
-
requestedSkill: requestedSkills[0],
|
|
2294
|
-
availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
|
|
2295
|
-
ignoredPublishedSkills: {
|
|
2296
|
-
scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
|
|
2297
|
-
count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
|
|
2298
|
-
}
|
|
2299
|
-
}, null, 2),
|
|
2300
|
-
metadata: {
|
|
2301
|
-
status: 'not_found',
|
|
2302
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
2307
|
-
workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
|
|
2308
|
-
item,
|
|
2309
|
-
signal: context.abort,
|
|
2310
|
-
useCache: !args.refresh,
|
|
2311
|
-
purpose: 'TOOL_FETCH'
|
|
2312
|
-
})));
|
|
2313
|
-
if (skillDetailResults.some(result => !result.ok && result.status === 'missing_auth')) {
|
|
2314
|
-
try {
|
|
2315
|
-
await startLoginCompletion('fetch').then(async authState => {
|
|
2316
|
-
await schedulePresenceStart(authState);
|
|
2317
|
-
});
|
|
2318
|
-
skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
2319
|
-
workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
|
|
2320
|
-
item,
|
|
2321
|
-
signal: context.abort,
|
|
2322
|
-
useCache: false,
|
|
2323
|
-
purpose: 'TOOL_FETCH'
|
|
2324
|
-
})));
|
|
2325
|
-
} catch {
|
|
2326
|
-
// Return the original detail failure after the login bootstrap attempt updates snapshot state.
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
const failedSkillDetail = skillDetailResults.find(result => !result.ok);
|
|
2330
|
-
if (failedSkillDetail && !failedSkillDetail.ok) {
|
|
2331
|
-
await emitFetchOutcome('FETCH_FAILED');
|
|
2332
|
-
return failedSkillDetail;
|
|
2333
|
-
}
|
|
2334
|
-
const skillDetails = skillDetailResults.map(result => {
|
|
2335
|
-
if (!result.ok) {
|
|
2336
|
-
throw new Error('Published skill detail result unexpectedly missing after success guard.');
|
|
2337
|
-
}
|
|
2338
|
-
return result.detail;
|
|
2339
|
-
});
|
|
2340
|
-
if (isSingleRequest && skillDetails[0]) {
|
|
2341
|
-
const detail = skillDetails[0];
|
|
2342
|
-
context.metadata({
|
|
2343
|
-
title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
|
|
2344
|
-
metadata: {
|
|
2345
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2346
|
-
skillSlug: detail.skillSlug,
|
|
2347
|
-
version: detail.version
|
|
2348
|
-
}
|
|
2349
|
-
});
|
|
2350
|
-
await emitFetchOutcome('FETCH_SUCCESS');
|
|
2351
|
-
return {
|
|
2352
|
-
output: JSON.stringify({
|
|
2353
|
-
pluginId: PLUGIN_ID,
|
|
2354
|
-
runtimeMode: 'tool_fetch_only',
|
|
2355
|
-
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2356
|
-
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2357
|
-
workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
|
|
2358
|
-
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2359
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2360
|
-
skill: detail
|
|
2361
|
-
}, null, 2),
|
|
2362
|
-
metadata: {
|
|
2363
|
-
status: 'ready',
|
|
2364
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2365
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2366
|
-
skillSlug: detail.skillSlug
|
|
2367
|
-
}
|
|
2368
|
-
};
|
|
2369
|
-
}
|
|
2370
|
-
context.metadata({
|
|
2371
|
-
title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
|
|
2372
|
-
metadata: {
|
|
2373
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2374
|
-
requestedCount: requestedSkills.length.toString(),
|
|
2375
|
-
matchedCount: skillDetails.length.toString()
|
|
2376
|
-
}
|
|
2377
|
-
});
|
|
2378
|
-
await emitFetchOutcome('FETCH_SUCCESS');
|
|
2379
|
-
return {
|
|
2380
|
-
output: JSON.stringify({
|
|
2381
|
-
pluginId: PLUGIN_ID,
|
|
2382
|
-
runtimeMode: 'tool_fetch_only',
|
|
2383
|
-
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2384
|
-
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2385
|
-
workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
|
|
2386
|
-
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2387
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2388
|
-
requestedSkills,
|
|
2389
|
-
missingSkills: selection.missingIdentifiers,
|
|
2390
|
-
skills: skillDetails
|
|
2391
|
-
}, null, 2),
|
|
2392
|
-
metadata: {
|
|
2393
|
-
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
2394
|
-
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2395
|
-
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2396
|
-
matchedCount: skillDetails.length.toString()
|
|
2397
|
-
}
|
|
2398
|
-
};
|
|
2399
|
-
};
|
|
2400
|
-
const executeStatusTool = async ({
|
|
2401
|
-
args,
|
|
2402
|
-
context
|
|
2403
|
-
}) => {
|
|
2404
|
-
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
2405
|
-
let snapshot = await resolvePluginStatusSnapshot({
|
|
2406
|
-
worktree: input.worktree,
|
|
2407
|
-
directory: requestedDirectory,
|
|
2408
|
-
signal: context.abort
|
|
2409
|
-
});
|
|
2410
|
-
if (snapshot.status === 'missing_auth') {
|
|
2411
|
-
try {
|
|
2412
|
-
await startLoginCompletion('status').then(async authState => {
|
|
2413
|
-
await schedulePresenceStart(authState);
|
|
2414
|
-
});
|
|
2415
|
-
snapshot = await resolvePluginStatusSnapshot({
|
|
2416
|
-
worktree: input.worktree,
|
|
2417
|
-
directory: requestedDirectory,
|
|
2418
|
-
signal: context.abort
|
|
2419
|
-
});
|
|
2420
|
-
} catch {
|
|
2421
|
-
// Keep returning the safe missing-auth snapshot when interactive login is cancelled or fails.
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
if (snapshot.status === 'ready') {
|
|
2425
|
-
await scheduleInteractivePresenceStart();
|
|
2426
|
-
}
|
|
2427
|
-
const metadata = toPluginStatusMetadata(snapshot);
|
|
2428
|
-
context.metadata({
|
|
2429
|
-
title: `opencode-wizard status: ${snapshot.status} / auth ${snapshot.authState.status}`,
|
|
2430
|
-
metadata
|
|
2431
|
-
});
|
|
2432
|
-
return {
|
|
2433
|
-
output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
|
|
2434
|
-
metadata
|
|
2435
|
-
};
|
|
2436
|
-
};
|
|
2437
|
-
const executePublishedSkillPreferenceTool = async ({
|
|
2438
|
-
args,
|
|
2439
|
-
context
|
|
2440
|
-
}) => {
|
|
2441
|
-
const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
|
|
2442
|
-
const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
|
|
2443
|
-
lastInteractiveDirectoryPath = directoryPath;
|
|
2444
|
-
const requestedSkill = typeof args.skill === 'string' ? args.skill.trim() : '';
|
|
2445
|
-
const emitPreferenceOutcome = async event => {
|
|
2446
|
-
await emitActionEventForCurrentSession({
|
|
2447
|
-
event,
|
|
2448
|
-
directoryPath
|
|
2449
|
-
});
|
|
2450
|
-
};
|
|
2451
|
-
try {
|
|
2452
|
-
if (!requestedSkill) {
|
|
2453
|
-
throw new Error('Published skill preference tool requires a non-empty skill slug, artifact name, or skill name.');
|
|
2454
|
-
}
|
|
2455
|
-
if (typeof args.action !== 'string') {
|
|
2456
|
-
throw new Error('Published skill preference tool requires an action: install, uninstall, ignore, or unignore.');
|
|
2457
|
-
}
|
|
2458
|
-
const action = toPublishedSkillPreferenceAction(args.action);
|
|
2459
|
-
const catalogResult = await loadPublishedSkillCatalog({
|
|
2460
|
-
directory: requestedDirectory,
|
|
2461
|
-
useCache: true,
|
|
2462
|
-
signal: context.abort
|
|
2463
|
-
});
|
|
2464
|
-
if (!catalogResult.fetchResult.ok) {
|
|
2465
|
-
throw new Error(`Cannot resolve published skill preference target: ${catalogResult.fetchResult.message}`);
|
|
2466
|
-
}
|
|
2467
|
-
const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
|
|
2468
|
-
...item,
|
|
2469
|
-
assignmentSource: 'CATALOG',
|
|
2470
|
-
assignmentType: 'PATH',
|
|
2471
|
-
scopePath: '',
|
|
2472
|
-
includeChildren: true
|
|
2473
|
-
}));
|
|
2474
|
-
const preferenceSelection = selectPublishedSkills({
|
|
2475
|
-
...catalogResult.fetchResult.payload,
|
|
2476
|
-
skills: [...catalogResult.fetchResult.payload.skills, ...selectableCatalogSkills, ...catalogResult.fetchResult.payload.userPreferences.ignoredSkills]
|
|
2477
|
-
}, [requestedSkill]);
|
|
2478
|
-
const matchedSkill = preferenceSelection.selectedItems[0];
|
|
2479
|
-
if (!matchedSkill) {
|
|
2480
|
-
throw new Error(`Published skill preference target was not found for identifier: ${requestedSkill}.`);
|
|
2481
|
-
}
|
|
2482
|
-
const skillSlug = matchedSkill.skill.slug;
|
|
2483
|
-
const preferenceState = action === 'ignore' || action === 'unignore' ? await setPublishedSkillIgnored({
|
|
2484
|
-
worktree: input.worktree,
|
|
2485
|
-
directory: requestedDirectory,
|
|
2486
|
-
skillSlug,
|
|
2487
|
-
ignored: action === 'ignore',
|
|
2488
|
-
preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
|
|
2489
|
-
}) : await setPublishedSkillInstalled({
|
|
2490
|
-
worktree: input.worktree,
|
|
2491
|
-
directory: requestedDirectory,
|
|
2492
|
-
skillSlug,
|
|
2493
|
-
installed: action === 'install',
|
|
2494
|
-
preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
|
|
2495
|
-
});
|
|
2496
|
-
await scheduleInteractivePresenceStart();
|
|
2497
|
-
await emitPreferenceOutcome('PREFERENCE_SUCCESS');
|
|
2498
|
-
const metadata = {
|
|
2499
|
-
status: 'updated',
|
|
2500
|
-
skillSlug,
|
|
2501
|
-
action,
|
|
2502
|
-
directoryPath,
|
|
2503
|
-
ignoredSkillCount: preferenceState.ignoredSkillSlugs.length.toString()
|
|
2504
|
-
};
|
|
2505
|
-
context.metadata({
|
|
2506
|
-
title: `opencode-wizard published skill preference: ${action} ${skillSlug}`,
|
|
2507
|
-
metadata
|
|
2508
|
-
});
|
|
2509
|
-
return {
|
|
2510
|
-
output: JSON.stringify({
|
|
2511
|
-
pluginId: PLUGIN_ID,
|
|
2512
|
-
status: 'updated',
|
|
2513
|
-
requestedIdentifier: requestedSkill,
|
|
2514
|
-
skillSlug,
|
|
2515
|
-
action,
|
|
2516
|
-
requestedDirectoryPath: directoryPath,
|
|
2517
|
-
preferenceState,
|
|
2518
|
-
message: 'Published skill preference updated through the shared server-backed API; TUI views will reflect this after refresh.'
|
|
2519
|
-
}, null, 2),
|
|
2520
|
-
metadata
|
|
2521
|
-
};
|
|
2522
|
-
} catch (error) {
|
|
2523
|
-
await emitPreferenceOutcome('PREFERENCE_FAILED');
|
|
2524
|
-
throw error;
|
|
2525
|
-
}
|
|
2526
|
-
};
|
|
2527
|
-
return {
|
|
2528
|
-
tool: {
|
|
2529
|
-
opencode_wizard_published_skills_fetch: tool({
|
|
2530
|
-
description: 'Fetch one or multiple wizard-published skill bodies/details for the current scope. Use this for wizard-listed/private/scoped/backend-published skill slugs instead of the native OpenCode skill tool, and after native errors like `Skill "..." not found`; prefer `skills` for multiple identifiers and call with no args to discover the catalog and bootstrap auth when needed',
|
|
2531
|
-
args: {
|
|
2532
|
-
skill: tool.schema.string().optional().describe('Single skill slug, artifact name, or skill name; backward-compatible with comma/newline-delimited lists'),
|
|
2533
|
-
skills: tool.schema.string().optional().describe('One or more comma-separated or newline-separated skill slugs, artifact names, or skill names'),
|
|
2534
|
-
directory: tool.schema.string().optional().describe('Optional absolute or relative directory override'),
|
|
2535
|
-
refresh: tool.schema.boolean().optional().describe('Bypass the local plugin cache for this request')
|
|
2536
|
-
},
|
|
2537
|
-
async execute(args, context) {
|
|
2538
|
-
return executePublishedSkillsFetchTool({
|
|
2539
|
-
args,
|
|
2540
|
-
context
|
|
2541
|
-
});
|
|
2542
|
-
}
|
|
2543
|
-
}),
|
|
2544
|
-
opencode_wizard_published_skill_preference_set: tool({
|
|
2545
|
-
description: 'Install, uninstall, ignore, or unignore a backend-published wizard skill for non-TUI workflows using the same shared server-backed preference API as the TUI overlay',
|
|
2546
|
-
args: {
|
|
2547
|
-
skill: tool.schema.string().describe('Published skill slug, artifact name, or skill name to update'),
|
|
2548
|
-
action: tool.schema.string().describe('Preference action: install, uninstall, ignore, or unignore'),
|
|
2549
|
-
preferenceScope: tool.schema.string().optional().describe('Preference scope for the action: project or global; defaults to project'),
|
|
2550
|
-
directory: tool.schema.string().optional().describe('Optional absolute or relative directory override')
|
|
2551
|
-
},
|
|
2552
|
-
async execute(args, context) {
|
|
2553
|
-
return executePublishedSkillPreferenceTool({
|
|
2554
|
-
args,
|
|
2555
|
-
context
|
|
2556
|
-
});
|
|
2557
|
-
}
|
|
2558
|
-
}),
|
|
2559
|
-
opencode_wizard_status: tool({
|
|
2560
|
-
description: 'Report opencode-wizard plugin status, bootstrap auth when missing, and return a safe auth summary without exposing tokens',
|
|
2561
|
-
args: {
|
|
2562
|
-
directory: tool.schema.string().optional().describe('Optional absolute or relative directory override')
|
|
2563
|
-
},
|
|
2564
|
-
async execute(args, context) {
|
|
2565
|
-
return executeStatusTool({
|
|
2566
|
-
args,
|
|
2567
|
-
context
|
|
2568
|
-
});
|
|
2569
|
-
}
|
|
2570
|
-
})
|
|
2571
|
-
},
|
|
2572
|
-
'experimental.chat.system.transform': async (_hookInput, output) => {
|
|
2573
|
-
let publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
2574
|
-
directory: input.directory,
|
|
2575
|
-
useCache: true,
|
|
2576
|
-
signal: AbortSignal.timeout(5_000)
|
|
2577
|
-
});
|
|
2578
|
-
if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
|
|
2579
|
-
try {
|
|
2580
|
-
await startLoginCompletion('status').then(async authState => {
|
|
2581
|
-
await schedulePresenceStart(authState);
|
|
2582
|
-
});
|
|
2583
|
-
publishedSkillsResult = await loadPublishedSkillCatalog({
|
|
2584
|
-
directory: input.directory,
|
|
2585
|
-
useCache: false,
|
|
2586
|
-
signal: AbortSignal.timeout(5_000)
|
|
2587
|
-
});
|
|
2588
|
-
} catch {
|
|
2589
|
-
const loginMessage = loginBootstrap.snapshot.message ? ` Last login status: ${loginBootstrap.snapshot.message}` : '';
|
|
2590
|
-
output.system.push(`opencode-wizard plugin stored auth is missing, expired, or rejected. Startup browser login was started but did not complete successfully.${loginMessage} Use opencode_wizard_status or opencode_wizard_published_skills_fetch to retry authentication when published skills are needed. No tokens are exposed.`);
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
if (!publishedSkillsResult.fetchResult.ok) {
|
|
2594
|
-
output.system.push(`opencode-wizard plugin startup login completed, but published skills are still unavailable: ${publishedSkillsResult.fetchResult.message} No tokens are exposed.`);
|
|
2595
|
-
return;
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2598
|
-
if (publishedSkillsResult.fetchResult.ok) {
|
|
2599
|
-
await scheduleInteractivePresenceStart();
|
|
2600
|
-
}
|
|
2601
|
-
const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
2602
|
-
const details = await loadSystemNoteDetails({
|
|
2603
|
-
publishedSkillsResult: filteredPublishedSkillsResult,
|
|
2604
|
-
signal: AbortSignal.timeout(5_000)
|
|
2605
|
-
});
|
|
2606
|
-
const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, details);
|
|
2607
|
-
if (!systemNote) return;
|
|
2608
|
-
output.system.push(systemNote);
|
|
2609
|
-
}
|
|
2610
|
-
};
|
|
2611
|
-
};
|
|
1
|
+
export { AVAILABLE_PUBLISHED_SKILL_TOOLS, resolveAvailableTools } from './plugin-tools.js';
|
|
2
|
+
export { buildSkillMarkdown, parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail } from './published-skills-transform.js';
|
|
3
|
+
export { PLUGIN_ID, NATIVE_SKILLS_URL_COMPATIBILITY, buildSystemNote, resolveConfig, resolvePluginStatusSnapshot, resolvePluginStatusSnapshotWithAuthBootstrap, setPublishedSkillIgnored, setPublishedSkillInstalled, toPluginAuthStateSummary, toPublishedSkillCatalog } from './server/runtime.js';
|
|
4
|
+
import { PLUGIN_ID, OpencodeWizardSkillsPlugin } from './server/runtime.js';
|
|
2612
5
|
export default {
|
|
2613
6
|
id: PLUGIN_ID,
|
|
2614
7
|
server: OpencodeWizardSkillsPlugin
|