@aexol/opencode-wizard 0.1.12 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/server.d.ts +44 -3
- package/dist/server.js +714 -93
- package/dist/server.js.map +1 -1
- package/dist/smoke-published-skills.js +7 -1
- package/dist/smoke-published-skills.js.map +1 -1
- package/dist/tui.js +350 -185
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import crypto from 'node:crypto';
|
|
5
6
|
import { execFile } from 'node:child_process';
|
|
@@ -11,7 +12,8 @@ const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
|
|
|
11
12
|
export const PLUGIN_ID = 'opencode-wizard';
|
|
12
13
|
const CACHE_TTL_MS = 30_000;
|
|
13
14
|
const ROOT_SKILL_SEED_PATH = '.opencode/skills';
|
|
14
|
-
const
|
|
15
|
+
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
|
|
16
|
+
const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
|
|
15
17
|
const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
|
|
16
18
|
const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
|
|
17
19
|
const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
|
|
@@ -52,6 +54,7 @@ const statusPathLoginBootstrap = {
|
|
|
52
54
|
};
|
|
53
55
|
const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
|
|
54
56
|
export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_status'];
|
|
57
|
+
let publishedSkillPreferenceCacheVersion = 0;
|
|
55
58
|
export const NATIVE_SKILLS_URL_COMPATIBILITY = {
|
|
56
59
|
configKey: 'skills.urls',
|
|
57
60
|
deliveryMode: 'public_static_registry',
|
|
@@ -112,6 +115,137 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
|
|
|
112
115
|
publishedAt
|
|
113
116
|
}
|
|
114
117
|
}
|
|
118
|
+
catalogSkills {
|
|
119
|
+
skill {
|
|
120
|
+
id
|
|
121
|
+
slug
|
|
122
|
+
name
|
|
123
|
+
summary
|
|
124
|
+
whenToUse
|
|
125
|
+
status
|
|
126
|
+
installPolicy
|
|
127
|
+
tags {
|
|
128
|
+
id
|
|
129
|
+
slug
|
|
130
|
+
label
|
|
131
|
+
description
|
|
132
|
+
facet {
|
|
133
|
+
id
|
|
134
|
+
slug
|
|
135
|
+
label
|
|
136
|
+
description
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
skillVersion {
|
|
141
|
+
id
|
|
142
|
+
version
|
|
143
|
+
title
|
|
144
|
+
summary
|
|
145
|
+
status
|
|
146
|
+
}
|
|
147
|
+
publishedArtifact {
|
|
148
|
+
id
|
|
149
|
+
frontmatterName
|
|
150
|
+
frontmatterDescription
|
|
151
|
+
checksum
|
|
152
|
+
publishedAt
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
userPreferences {
|
|
156
|
+
scopeKey
|
|
157
|
+
userKey
|
|
158
|
+
ignoredSkills {
|
|
159
|
+
assignmentSource
|
|
160
|
+
assignmentType
|
|
161
|
+
scopePath
|
|
162
|
+
includeChildren
|
|
163
|
+
skill {
|
|
164
|
+
id
|
|
165
|
+
slug
|
|
166
|
+
name
|
|
167
|
+
summary
|
|
168
|
+
whenToUse
|
|
169
|
+
status
|
|
170
|
+
installPolicy
|
|
171
|
+
tags {
|
|
172
|
+
id
|
|
173
|
+
slug
|
|
174
|
+
label
|
|
175
|
+
description
|
|
176
|
+
facet {
|
|
177
|
+
id
|
|
178
|
+
slug
|
|
179
|
+
label
|
|
180
|
+
description
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
skillVersion {
|
|
185
|
+
id
|
|
186
|
+
version
|
|
187
|
+
title
|
|
188
|
+
summary
|
|
189
|
+
status
|
|
190
|
+
}
|
|
191
|
+
publishedArtifact {
|
|
192
|
+
id
|
|
193
|
+
frontmatterName
|
|
194
|
+
frontmatterDescription
|
|
195
|
+
checksum
|
|
196
|
+
publishedAt
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
|
|
204
|
+
mutation SetPublishedSkillPreference($input: SetPublishedSkillPreferenceInput!) {
|
|
205
|
+
setPublishedSkillPreference(input: $input) {
|
|
206
|
+
scopeKey
|
|
207
|
+
userKey
|
|
208
|
+
ignoredSkills {
|
|
209
|
+
assignmentSource
|
|
210
|
+
assignmentType
|
|
211
|
+
scopePath
|
|
212
|
+
includeChildren
|
|
213
|
+
skill {
|
|
214
|
+
id
|
|
215
|
+
slug
|
|
216
|
+
name
|
|
217
|
+
summary
|
|
218
|
+
whenToUse
|
|
219
|
+
status
|
|
220
|
+
installPolicy
|
|
221
|
+
tags {
|
|
222
|
+
id
|
|
223
|
+
slug
|
|
224
|
+
label
|
|
225
|
+
description
|
|
226
|
+
facet {
|
|
227
|
+
id
|
|
228
|
+
slug
|
|
229
|
+
label
|
|
230
|
+
description
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
skillVersion {
|
|
235
|
+
id
|
|
236
|
+
version
|
|
237
|
+
title
|
|
238
|
+
summary
|
|
239
|
+
status
|
|
240
|
+
}
|
|
241
|
+
publishedArtifact {
|
|
242
|
+
id
|
|
243
|
+
frontmatterName
|
|
244
|
+
frontmatterDescription
|
|
245
|
+
checksum
|
|
246
|
+
publishedAt
|
|
247
|
+
}
|
|
248
|
+
}
|
|
115
249
|
}
|
|
116
250
|
}
|
|
117
251
|
`;
|
|
@@ -208,7 +342,7 @@ export const resolveConfig = async worktree => {
|
|
|
208
342
|
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
209
343
|
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
210
344
|
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
211
|
-
authStatePath:
|
|
345
|
+
authStatePath: GLOBAL_CONFIG_PATH
|
|
212
346
|
};
|
|
213
347
|
};
|
|
214
348
|
const normalizeAbsolutePath = value => path.resolve(value);
|
|
@@ -332,15 +466,89 @@ const isAuthState = value => {
|
|
|
332
466
|
if (!isRecord(value)) return false;
|
|
333
467
|
return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
|
|
334
468
|
};
|
|
335
|
-
const
|
|
469
|
+
const readGlobalConfig = async configFile => {
|
|
470
|
+
const storedConfig = await readJsonFile(configFile);
|
|
471
|
+
if (isRecord(storedConfig)) return storedConfig;
|
|
472
|
+
return {};
|
|
473
|
+
};
|
|
474
|
+
const writeGlobalConfig = async (configFile, config) => {
|
|
475
|
+
await writeJsonFile(configFile, config);
|
|
476
|
+
};
|
|
477
|
+
const withoutLegacyPublishedSkillPreferences = config => {
|
|
478
|
+
const {
|
|
479
|
+
publishedSkillPreferences,
|
|
480
|
+
ignoredPublishedSkills,
|
|
481
|
+
...safeConfig
|
|
482
|
+
} = config;
|
|
483
|
+
void publishedSkillPreferences;
|
|
484
|
+
void ignoredPublishedSkills;
|
|
485
|
+
return safeConfig;
|
|
486
|
+
};
|
|
487
|
+
const hasLegacyPublishedSkillPreferences = config => {
|
|
488
|
+
return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
|
|
489
|
+
};
|
|
490
|
+
const readGlobalAuthState = async configFile => {
|
|
491
|
+
const storedConfig = await readGlobalConfig(configFile);
|
|
492
|
+
const storedAuthState = storedConfig.auth;
|
|
493
|
+
if (storedAuthState === undefined || storedAuthState === null) return null;
|
|
494
|
+
if (isAuthState(storedAuthState)) {
|
|
495
|
+
if (hasLegacyPublishedSkillPreferences(storedConfig)) {
|
|
496
|
+
await writeGlobalConfig(configFile, withoutLegacyPublishedSkillPreferences(storedConfig));
|
|
497
|
+
}
|
|
498
|
+
return storedAuthState;
|
|
499
|
+
}
|
|
500
|
+
await writeGlobalConfig(configFile, {
|
|
501
|
+
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
502
|
+
auth: null
|
|
503
|
+
});
|
|
504
|
+
return null;
|
|
505
|
+
};
|
|
506
|
+
const readLegacyAuthState = async authStateFile => {
|
|
336
507
|
const storedAuthState = await readJsonFile(authStateFile);
|
|
337
508
|
if (storedAuthState === null) return null;
|
|
338
509
|
if (isAuthState(storedAuthState)) return storedAuthState;
|
|
339
510
|
await deleteFileIfExists(authStateFile);
|
|
340
511
|
return null;
|
|
341
512
|
};
|
|
342
|
-
const writeAuthState = async (
|
|
343
|
-
await
|
|
513
|
+
const writeAuthState = async (configFile, authState) => {
|
|
514
|
+
const storedConfig = await readGlobalConfig(configFile);
|
|
515
|
+
await writeGlobalConfig(configFile, {
|
|
516
|
+
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
517
|
+
auth: authState
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
const clearAuthState = async configFile => {
|
|
521
|
+
const storedConfig = await readGlobalConfig(configFile);
|
|
522
|
+
await writeGlobalConfig(configFile, {
|
|
523
|
+
...withoutLegacyPublishedSkillPreferences(storedConfig),
|
|
524
|
+
auth: null
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
const toIgnoredSkillSlug = value => {
|
|
528
|
+
const normalized = value.trim().toLowerCase();
|
|
529
|
+
if (!normalized) return null;
|
|
530
|
+
return normalized;
|
|
531
|
+
};
|
|
532
|
+
const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
|
|
533
|
+
const workspaceSlug = payload?.workspace?.slug ?? resolution.fallbackWorkspaceSlug;
|
|
534
|
+
if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
|
|
535
|
+
if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
|
|
536
|
+
return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
|
|
537
|
+
};
|
|
538
|
+
const toStoredUserKey = authState => {
|
|
539
|
+
if (authState?.userId) return authState.userId;
|
|
540
|
+
if (authState?.email) return authState.email.toLowerCase();
|
|
541
|
+
return 'anonymous';
|
|
542
|
+
};
|
|
543
|
+
const resolvePublishedSkillPreferenceCacheContext = async config => {
|
|
544
|
+
const authState = await readGlobalAuthState(config.authStatePath);
|
|
545
|
+
return {
|
|
546
|
+
userKey: toStoredUserKey(authState),
|
|
547
|
+
preferenceVersion: publishedSkillPreferenceCacheVersion
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
|
|
551
|
+
return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
|
|
344
552
|
};
|
|
345
553
|
const toAuthState = session => ({
|
|
346
554
|
pluginId: PLUGIN_ID,
|
|
@@ -351,16 +559,24 @@ const toAuthState = session => ({
|
|
|
351
559
|
email: session.user.email
|
|
352
560
|
});
|
|
353
561
|
const resolveStoredAuthState = async (worktree, config) => {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
562
|
+
const authState = await readGlobalAuthState(config.authStatePath);
|
|
563
|
+
if (authState && Date.parse(authState.expiresAt) > Date.now()) {
|
|
564
|
+
return authState;
|
|
565
|
+
}
|
|
566
|
+
if (authState) {
|
|
567
|
+
await clearAuthState(config.authStatePath);
|
|
357
568
|
return null;
|
|
358
569
|
}
|
|
359
|
-
|
|
360
|
-
|
|
570
|
+
const legacyAuthStateFile = path.resolve(worktree, LEGACY_AUTH_STATE_PATH);
|
|
571
|
+
const legacyAuthState = await readLegacyAuthState(legacyAuthStateFile);
|
|
572
|
+
if (!legacyAuthState) return null;
|
|
573
|
+
if (Date.parse(legacyAuthState.expiresAt) <= Date.now()) {
|
|
574
|
+
await deleteFileIfExists(legacyAuthStateFile);
|
|
575
|
+
return null;
|
|
361
576
|
}
|
|
362
|
-
await
|
|
363
|
-
|
|
577
|
+
await writeAuthState(config.authStatePath, legacyAuthState);
|
|
578
|
+
await deleteFileIfExists(legacyAuthStateFile);
|
|
579
|
+
return legacyAuthState;
|
|
364
580
|
};
|
|
365
581
|
export const buildSkillMarkdown = item => {
|
|
366
582
|
const artifactBody = item.publishedArtifact.markdownBody.trim();
|
|
@@ -435,11 +651,12 @@ const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) =
|
|
|
435
651
|
other: 0
|
|
436
652
|
});
|
|
437
653
|
const getSkillContextKind = item => {
|
|
438
|
-
if (item.assignmentSource === 'GLOBAL') return 'global';
|
|
654
|
+
if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
|
|
439
655
|
return 'project';
|
|
440
656
|
};
|
|
441
657
|
const getSkillPolicyLabel = (policy, contextKind) => {
|
|
442
658
|
if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
|
|
659
|
+
if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
|
|
443
660
|
if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active global assignment';
|
|
444
661
|
return 'PROJECT_INSTALLABLE · active project/workspace assignment';
|
|
445
662
|
};
|
|
@@ -474,10 +691,35 @@ export const toPublishedSkillDetail = item => ({
|
|
|
474
691
|
markdownBody: item.publishedArtifact.markdownBody,
|
|
475
692
|
renderedContent: item.publishedArtifact.renderedContent
|
|
476
693
|
});
|
|
694
|
+
const toInstallableSkillSummary = item => ({
|
|
695
|
+
skillSlug: item.skill.slug,
|
|
696
|
+
skillName: item.skill.name,
|
|
697
|
+
artifactName: item.publishedArtifact.frontmatterName,
|
|
698
|
+
artifactDescription: item.publishedArtifact.frontmatterDescription,
|
|
699
|
+
whenToUse: item.skill.whenToUse ?? null,
|
|
700
|
+
version: item.skillVersion.version,
|
|
701
|
+
assignmentSource: 'CATALOG',
|
|
702
|
+
assignmentType: 'PATH',
|
|
703
|
+
scopePath: '',
|
|
704
|
+
includeChildren: true,
|
|
705
|
+
checksum: item.publishedArtifact.checksum,
|
|
706
|
+
publishedAt: item.publishedArtifact.publishedAt,
|
|
707
|
+
identifiers: getSkillIdentifiers({
|
|
708
|
+
...item,
|
|
709
|
+
assignmentSource: 'CATALOG',
|
|
710
|
+
assignmentType: 'PATH',
|
|
711
|
+
scopePath: '',
|
|
712
|
+
includeChildren: true
|
|
713
|
+
}),
|
|
714
|
+
tags: item.skill.tags.map(toPublishedSkillTagSummary),
|
|
715
|
+
contextKind: 'installable',
|
|
716
|
+
installPolicy: item.skill.installPolicy,
|
|
717
|
+
policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable')
|
|
718
|
+
});
|
|
477
719
|
export const toPublishedSkillCatalog = payload => ({
|
|
478
720
|
pluginId: PLUGIN_ID,
|
|
479
721
|
runtimeMode: 'tool_fetch_only',
|
|
480
|
-
deliveryModel: '
|
|
722
|
+
deliveryModel: 'backend_published_installed_effective_skills',
|
|
481
723
|
workspace: payload.workspace,
|
|
482
724
|
directoryPath: payload.directoryPath,
|
|
483
725
|
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
@@ -487,6 +729,40 @@ export const toPublishedSkillCatalog = payload => ({
|
|
|
487
729
|
facets: getPublishedSkillFacets(payload.skills),
|
|
488
730
|
skills: payload.skills.map(toPublishedSkillSummary)
|
|
489
731
|
});
|
|
732
|
+
const filterIgnoredPublishedSkills = async (config, result) => {
|
|
733
|
+
const authState = await readGlobalAuthState(config.authStatePath);
|
|
734
|
+
const userKey = toStoredUserKey(authState);
|
|
735
|
+
if (!result.fetchResult.ok) {
|
|
736
|
+
return {
|
|
737
|
+
...result,
|
|
738
|
+
ignoreState: {
|
|
739
|
+
scopeKey: getPublishedSkillIgnoreScopeKey(result.workspaceResolution),
|
|
740
|
+
userKey,
|
|
741
|
+
ignoredSkillSlugs: [],
|
|
742
|
+
installedGlobalSkillSlugs: [],
|
|
743
|
+
installedWorkspaceSkillSlugs: []
|
|
744
|
+
},
|
|
745
|
+
ignoredSkills: []
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const ignoredSkills = result.fetchResult.payload.userPreferences.ignoredSkills.map(toPublishedSkillSummary);
|
|
749
|
+
const ignoredSkillSlugs = ignoredSkills.map(skill => skill.skillSlug);
|
|
750
|
+
return {
|
|
751
|
+
...result,
|
|
752
|
+
ignoreState: {
|
|
753
|
+
scopeKey: result.fetchResult.payload.userPreferences.scopeKey,
|
|
754
|
+
userKey: result.fetchResult.payload.userPreferences.userKey || userKey,
|
|
755
|
+
ignoredSkillSlugs,
|
|
756
|
+
installedGlobalSkillSlugs: [],
|
|
757
|
+
installedWorkspaceSkillSlugs: []
|
|
758
|
+
},
|
|
759
|
+
ignoredSkills
|
|
760
|
+
};
|
|
761
|
+
};
|
|
762
|
+
const getWorkspaceUnavailableMessage = payload => {
|
|
763
|
+
if (payload.workspace) return null;
|
|
764
|
+
return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
|
|
765
|
+
};
|
|
490
766
|
const normalizeSkillIdentifier = value => value.trim().toLowerCase();
|
|
491
767
|
const parseSkillIdentifiers = value => {
|
|
492
768
|
const seen = new Set();
|
|
@@ -568,7 +844,7 @@ const buildSystemNote = (result, config, details) => {
|
|
|
568
844
|
const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
|
|
569
845
|
const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
|
|
570
846
|
const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
|
|
571
|
-
return [`opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}
|
|
847
|
+
return [result.fetchResult.payload.workspace ? `opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.` : 'opencode-wizard published global skills are available from backend runtime delivery; workspace-specific skills are unavailable because the workspace was not found.', `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
|
|
572
848
|
};
|
|
573
849
|
const toWorkspaceResolutionOutput = resolution => ({
|
|
574
850
|
requestedDirectory: resolution.requestedDirectory,
|
|
@@ -585,6 +861,7 @@ const toWorkspaceResolutionMetadata = resolution => ({
|
|
|
585
861
|
});
|
|
586
862
|
const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
|
|
587
863
|
const authState = await resolveStoredAuthState(worktree, config);
|
|
864
|
+
const filteredResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
588
865
|
const base = {
|
|
589
866
|
pluginId: PLUGIN_ID,
|
|
590
867
|
runtimeMode: 'tool_fetch_only',
|
|
@@ -613,20 +890,26 @@ const formatStatusOutput = async (worktree, config, publishedSkillsResult, login
|
|
|
613
890
|
email: loginBootstrapSnapshot.email,
|
|
614
891
|
message: loginBootstrapSnapshot.message
|
|
615
892
|
},
|
|
616
|
-
status:
|
|
617
|
-
fetchedAt:
|
|
618
|
-
source:
|
|
619
|
-
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS
|
|
893
|
+
status: filteredResult.fetchResult.status,
|
|
894
|
+
fetchedAt: filteredResult.fetchResult.fetchedAt,
|
|
895
|
+
source: filteredResult.fetchResult.source,
|
|
896
|
+
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
897
|
+
ignoredPublishedSkills: {
|
|
898
|
+
scopeKey: filteredResult.ignoreState.scopeKey,
|
|
899
|
+
userKey: filteredResult.ignoreState.userKey,
|
|
900
|
+
count: filteredResult.ignoreState.ignoredSkillSlugs.length
|
|
901
|
+
}
|
|
620
902
|
};
|
|
621
|
-
if (!
|
|
903
|
+
if (!filteredResult.fetchResult.ok) {
|
|
622
904
|
return JSON.stringify({
|
|
623
905
|
...base,
|
|
624
|
-
message:
|
|
906
|
+
message: filteredResult.fetchResult.message
|
|
625
907
|
}, null, 2);
|
|
626
908
|
}
|
|
627
909
|
return JSON.stringify({
|
|
628
910
|
...base,
|
|
629
|
-
...toPublishedSkillCatalog(
|
|
911
|
+
...toPublishedSkillCatalog(filteredResult.fetchResult.payload),
|
|
912
|
+
message: getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload)
|
|
630
913
|
}, null, 2);
|
|
631
914
|
};
|
|
632
915
|
export const toPluginAuthStateSummary = authState => {
|
|
@@ -658,6 +941,11 @@ export const resolvePluginStatusSnapshot = async ({
|
|
|
658
941
|
directory
|
|
659
942
|
});
|
|
660
943
|
const fetchResult = await fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal);
|
|
944
|
+
const filteredResult = await filterIgnoredPublishedSkills(config, {
|
|
945
|
+
directoryPath: workspaceResolution.directoryPath,
|
|
946
|
+
workspaceResolution,
|
|
947
|
+
fetchResult
|
|
948
|
+
});
|
|
661
949
|
const authState = await resolveStoredAuthState(worktree, config);
|
|
662
950
|
return {
|
|
663
951
|
pluginId: PLUGIN_ID,
|
|
@@ -670,19 +958,43 @@ export const resolvePluginStatusSnapshot = async ({
|
|
|
670
958
|
rootSkillSeedPath: config.rootSkillSeedPath,
|
|
671
959
|
authStatePath: config.authStatePath,
|
|
672
960
|
authState: toPluginAuthStateSummary(authState),
|
|
673
|
-
status: fetchResult.status,
|
|
674
|
-
authMode: fetchResult.authMode,
|
|
675
|
-
fetchedAt: fetchResult.fetchedAt,
|
|
676
|
-
source: fetchResult.source,
|
|
961
|
+
status: filteredResult.fetchResult.status,
|
|
962
|
+
authMode: filteredResult.fetchResult.authMode,
|
|
963
|
+
fetchedAt: filteredResult.fetchResult.fetchedAt,
|
|
964
|
+
source: filteredResult.fetchResult.source,
|
|
677
965
|
availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
|
|
678
|
-
message: fetchResult.ok ?
|
|
679
|
-
catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
|
|
966
|
+
message: filteredResult.fetchResult.ok ? getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload) : filteredResult.fetchResult.message,
|
|
967
|
+
catalog: filteredResult.fetchResult.ok ? toPublishedSkillCatalog(filteredResult.fetchResult.payload) : null,
|
|
968
|
+
installableCatalog: filteredResult.fetchResult.ok ? {
|
|
969
|
+
count: filteredResult.fetchResult.payload.catalogSkills.length,
|
|
970
|
+
skills: filteredResult.fetchResult.payload.catalogSkills.map(toInstallableSkillSummary)
|
|
971
|
+
} : null,
|
|
972
|
+
ignoredPublishedSkills: {
|
|
973
|
+
scopeKey: filteredResult.ignoreState.scopeKey,
|
|
974
|
+
userKey: filteredResult.ignoreState.userKey,
|
|
975
|
+
count: filteredResult.ignoreState.ignoredSkillSlugs.length,
|
|
976
|
+
skills: filteredResult.ignoredSkills
|
|
977
|
+
}
|
|
680
978
|
};
|
|
681
979
|
};
|
|
682
980
|
const withStatusMessage = (snapshot, message) => ({
|
|
683
981
|
...snapshot,
|
|
684
982
|
message
|
|
685
983
|
});
|
|
984
|
+
const toAiFacingPluginStatusSnapshot = snapshot => {
|
|
985
|
+
const {
|
|
986
|
+
ignoredPublishedSkills,
|
|
987
|
+
installableCatalog: _installableCatalog,
|
|
988
|
+
...safeSnapshot
|
|
989
|
+
} = snapshot;
|
|
990
|
+
return {
|
|
991
|
+
...safeSnapshot,
|
|
992
|
+
ignoredPublishedSkills: {
|
|
993
|
+
scopeKey: ignoredPublishedSkills.scopeKey,
|
|
994
|
+
count: ignoredPublishedSkills.count
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
};
|
|
686
998
|
const startStatusPathLoginBootstrap = (worktree, config) => {
|
|
687
999
|
if (statusPathLoginBootstrap.promise) return;
|
|
688
1000
|
if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
|
|
@@ -714,7 +1026,7 @@ const startStatusPathLoginBootstrap = (worktree, config) => {
|
|
|
714
1026
|
signal: loginSignal
|
|
715
1027
|
});
|
|
716
1028
|
const authState = toAuthState(pluginSession);
|
|
717
|
-
await writeAuthState(
|
|
1029
|
+
await writeAuthState(config.authStatePath, authState);
|
|
718
1030
|
statusPathLoginBootstrap.status = 'authenticated';
|
|
719
1031
|
statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
|
|
720
1032
|
return authState;
|
|
@@ -749,6 +1061,93 @@ export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
|
|
|
749
1061
|
}
|
|
750
1062
|
return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
|
|
751
1063
|
};
|
|
1064
|
+
const toBackendPreferenceScope = preferenceScope => {
|
|
1065
|
+
if (preferenceScope === 'global') return 'GLOBAL';
|
|
1066
|
+
return 'WORKSPACE';
|
|
1067
|
+
};
|
|
1068
|
+
const setPublishedSkillPreference = async ({
|
|
1069
|
+
worktree,
|
|
1070
|
+
directory,
|
|
1071
|
+
config,
|
|
1072
|
+
skillSlug,
|
|
1073
|
+
preferenceScope,
|
|
1074
|
+
installed,
|
|
1075
|
+
ignored
|
|
1076
|
+
}) => {
|
|
1077
|
+
const workspaceResolution = await resolveWorkspace({
|
|
1078
|
+
config,
|
|
1079
|
+
directory
|
|
1080
|
+
});
|
|
1081
|
+
const response = await fetchPublishedSkillsGraphQl({
|
|
1082
|
+
worktree,
|
|
1083
|
+
config,
|
|
1084
|
+
query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
|
|
1085
|
+
variables: {
|
|
1086
|
+
input: {
|
|
1087
|
+
...toDeliveryInput(workspaceResolution),
|
|
1088
|
+
skillSlug,
|
|
1089
|
+
preferenceScope: toBackendPreferenceScope(preferenceScope),
|
|
1090
|
+
installed,
|
|
1091
|
+
ignored
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
|
|
1095
|
+
});
|
|
1096
|
+
if (!response.ok) {
|
|
1097
|
+
throw new Error(response.result.message);
|
|
1098
|
+
}
|
|
1099
|
+
const preferences = response.data.setPublishedSkillPreference;
|
|
1100
|
+
publishedSkillPreferenceCacheVersion += 1;
|
|
1101
|
+
return {
|
|
1102
|
+
scopeKey: preferences.scopeKey,
|
|
1103
|
+
userKey: preferences.userKey,
|
|
1104
|
+
ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
|
|
1105
|
+
installedGlobalSkillSlugs: [],
|
|
1106
|
+
installedWorkspaceSkillSlugs: []
|
|
1107
|
+
};
|
|
1108
|
+
};
|
|
1109
|
+
export const setPublishedSkillIgnored = async ({
|
|
1110
|
+
worktree,
|
|
1111
|
+
directory,
|
|
1112
|
+
skillSlug,
|
|
1113
|
+
ignored,
|
|
1114
|
+
preferenceScope
|
|
1115
|
+
}) => {
|
|
1116
|
+
const config = await resolveConfig(worktree);
|
|
1117
|
+
const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
|
|
1118
|
+
if (!normalizedSkillSlug) {
|
|
1119
|
+
throw new Error('Cannot toggle an empty published skill slug.');
|
|
1120
|
+
}
|
|
1121
|
+
return setPublishedSkillPreference({
|
|
1122
|
+
worktree,
|
|
1123
|
+
directory,
|
|
1124
|
+
config,
|
|
1125
|
+
skillSlug: normalizedSkillSlug,
|
|
1126
|
+
preferenceScope: preferenceScope ?? 'project',
|
|
1127
|
+
ignored
|
|
1128
|
+
});
|
|
1129
|
+
};
|
|
1130
|
+
export const setPublishedSkillInstalled = async ({
|
|
1131
|
+
worktree,
|
|
1132
|
+
directory,
|
|
1133
|
+
skillSlug,
|
|
1134
|
+
installed,
|
|
1135
|
+
preferenceScope
|
|
1136
|
+
}) => {
|
|
1137
|
+
const config = await resolveConfig(worktree);
|
|
1138
|
+
const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
|
|
1139
|
+
if (!normalizedSkillSlug) {
|
|
1140
|
+
throw new Error('Cannot toggle an empty published skill slug.');
|
|
1141
|
+
}
|
|
1142
|
+
return setPublishedSkillPreference({
|
|
1143
|
+
worktree,
|
|
1144
|
+
directory,
|
|
1145
|
+
config,
|
|
1146
|
+
skillSlug: normalizedSkillSlug,
|
|
1147
|
+
preferenceScope,
|
|
1148
|
+
installed
|
|
1149
|
+
});
|
|
1150
|
+
};
|
|
752
1151
|
const toPluginStatusMetadata = snapshot => ({
|
|
753
1152
|
backendOrigin: snapshot.backendOrigin,
|
|
754
1153
|
graphqlUrl: snapshot.graphqlUrl,
|
|
@@ -791,12 +1190,204 @@ const fetchOidcDiscoveryDocument = async signal => {
|
|
|
791
1190
|
}
|
|
792
1191
|
return await response.json();
|
|
793
1192
|
};
|
|
1193
|
+
const isCallbackPortInUseError = error => {
|
|
1194
|
+
if (!error || typeof error !== 'object') return false;
|
|
1195
|
+
if (!('code' in error)) return false;
|
|
1196
|
+
return error.code === 'EADDRINUSE';
|
|
1197
|
+
};
|
|
1198
|
+
const toCallbackServerStartError = error => {
|
|
1199
|
+
if (!isCallbackPortInUseError(error)) {
|
|
1200
|
+
return error instanceof Error ? error : new Error('Failed to start local OAuth callback server.');
|
|
1201
|
+
}
|
|
1202
|
+
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.');
|
|
1203
|
+
};
|
|
1204
|
+
const escapeHtml = value => {
|
|
1205
|
+
return value.replace(/[&<>'"]/g, character => {
|
|
1206
|
+
const replacements = {
|
|
1207
|
+
'&': '&',
|
|
1208
|
+
'<': '<',
|
|
1209
|
+
'>': '>',
|
|
1210
|
+
"'": ''',
|
|
1211
|
+
'"': '"'
|
|
1212
|
+
};
|
|
1213
|
+
return replacements[character] ?? character;
|
|
1214
|
+
});
|
|
1215
|
+
};
|
|
794
1216
|
const sendHtmlResponse = (response, statusCode, title, message) => {
|
|
1217
|
+
const escapedTitle = escapeHtml(title);
|
|
1218
|
+
const escapedMessage = escapeHtml(message);
|
|
1219
|
+
const isSuccess = statusCode >= 200 && statusCode < 300;
|
|
1220
|
+
const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';
|
|
1221
|
+
const cardTitle = isSuccess ? 'Authorization successful' : statusCode === 404 ? 'Callback not found' : 'Authorization failed';
|
|
1222
|
+
const escapedCardTitle = escapeHtml(cardTitle);
|
|
1223
|
+
const eyebrow = isSuccess ? 'Authorization complete' : statusCode === 404 ? 'Callback route not found' : 'Authorization needs attention';
|
|
1224
|
+
const actionText = isSuccess ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.' : 'You can close this window and return to OpenCode to try again.';
|
|
1225
|
+
const autoCloseScript = isSuccess ? `<script>
|
|
1226
|
+
window.setTimeout(() => window.close(), 2000);
|
|
1227
|
+
</script>` : '';
|
|
1228
|
+
const stateIcon = isSuccess ? '<path d="M7 12.5l3.1 3.1L17.5 8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>' : statusCode === 404 ? '<path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z" stroke="currentColor" stroke-width="2.2"/><path d="m15.5 15.5 4 4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>' : '<path d="M12 7v6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M12 17.2v.1" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>';
|
|
795
1229
|
response.writeHead(statusCode, {
|
|
796
1230
|
'content-type': 'text/html; charset=utf-8',
|
|
797
1231
|
'cache-control': 'no-store'
|
|
798
1232
|
});
|
|
799
|
-
response.end(`<!doctype html
|
|
1233
|
+
response.end(`<!doctype html>
|
|
1234
|
+
<html lang="en">
|
|
1235
|
+
<head>
|
|
1236
|
+
<meta charset="utf-8">
|
|
1237
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1238
|
+
<meta name="color-scheme" content="light dark">
|
|
1239
|
+
<title>${escapedTitle}</title>
|
|
1240
|
+
<style>
|
|
1241
|
+
:root {
|
|
1242
|
+
color-scheme: light dark;
|
|
1243
|
+
--page-bg: #f2efe7;
|
|
1244
|
+
--page-ink: #211d18;
|
|
1245
|
+
--muted: #6c6258;
|
|
1246
|
+
--panel: rgba(255, 252, 245, 0.82);
|
|
1247
|
+
--panel-border: rgba(78, 66, 52, 0.16);
|
|
1248
|
+
--success: #167848;
|
|
1249
|
+
--error: #ba3329;
|
|
1250
|
+
--not-found: #986614;
|
|
1251
|
+
--glow: rgba(22, 120, 72, 0.18);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
@media (prefers-color-scheme: dark) {
|
|
1255
|
+
:root {
|
|
1256
|
+
--page-bg: #12100d;
|
|
1257
|
+
--page-ink: #f7efe2;
|
|
1258
|
+
--muted: #b8aa98;
|
|
1259
|
+
--panel: rgba(30, 26, 22, 0.78);
|
|
1260
|
+
--panel-border: rgba(255, 244, 224, 0.14);
|
|
1261
|
+
--success: #71e0a6;
|
|
1262
|
+
--error: #ff897e;
|
|
1263
|
+
--not-found: #f7c96f;
|
|
1264
|
+
--glow: rgba(113, 224, 166, 0.2);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
* {
|
|
1269
|
+
box-sizing: border-box;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
body {
|
|
1273
|
+
min-height: 100vh;
|
|
1274
|
+
margin: 0;
|
|
1275
|
+
display: grid;
|
|
1276
|
+
place-items: center;
|
|
1277
|
+
padding: 24px;
|
|
1278
|
+
overflow: hidden;
|
|
1279
|
+
background:
|
|
1280
|
+
radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),
|
|
1281
|
+
radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),
|
|
1282
|
+
linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));
|
|
1283
|
+
color: var(--page-ink);
|
|
1284
|
+
font-family: ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
body::before {
|
|
1288
|
+
content: "";
|
|
1289
|
+
position: fixed;
|
|
1290
|
+
inset: -20%;
|
|
1291
|
+
pointer-events: none;
|
|
1292
|
+
background-image:
|
|
1293
|
+
linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),
|
|
1294
|
+
linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);
|
|
1295
|
+
background-size: 42px 42px;
|
|
1296
|
+
mask-image: radial-gradient(circle at center, black, transparent 68%);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
main {
|
|
1300
|
+
position: relative;
|
|
1301
|
+
width: min(100%, 560px);
|
|
1302
|
+
padding: clamp(28px, 7vw, 56px);
|
|
1303
|
+
border: 1px solid var(--panel-border);
|
|
1304
|
+
border-radius: 32px;
|
|
1305
|
+
background: var(--panel);
|
|
1306
|
+
box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);
|
|
1307
|
+
text-align: center;
|
|
1308
|
+
backdrop-filter: blur(18px) saturate(1.2);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.mark {
|
|
1312
|
+
width: 72px;
|
|
1313
|
+
height: 72px;
|
|
1314
|
+
margin: 0 auto 24px;
|
|
1315
|
+
display: grid;
|
|
1316
|
+
place-items: center;
|
|
1317
|
+
border-radius: 24px;
|
|
1318
|
+
color: var(--state-color);
|
|
1319
|
+
background: color-mix(in srgb, var(--state-color) 16%, transparent);
|
|
1320
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
[data-state="success"] { --state-color: var(--success); }
|
|
1324
|
+
[data-state="error"] { --state-color: var(--error); }
|
|
1325
|
+
[data-state="not-found"] { --state-color: var(--not-found); }
|
|
1326
|
+
|
|
1327
|
+
.eyebrow {
|
|
1328
|
+
margin: 0 0 10px;
|
|
1329
|
+
color: var(--state-color);
|
|
1330
|
+
font-size: 0.78rem;
|
|
1331
|
+
font-weight: 800;
|
|
1332
|
+
letter-spacing: 0.14em;
|
|
1333
|
+
text-transform: uppercase;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
h1 {
|
|
1337
|
+
margin: 0;
|
|
1338
|
+
font-size: clamp(2rem, 7vw, 3.35rem);
|
|
1339
|
+
line-height: 0.95;
|
|
1340
|
+
letter-spacing: -0.06em;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.message {
|
|
1344
|
+
margin: 22px auto 0;
|
|
1345
|
+
max-width: 38rem;
|
|
1346
|
+
color: var(--muted);
|
|
1347
|
+
font-size: clamp(1rem, 2.5vw, 1.1rem);
|
|
1348
|
+
line-height: 1.65;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
.next-step {
|
|
1352
|
+
margin: 26px 0 0;
|
|
1353
|
+
padding: 14px 18px;
|
|
1354
|
+
border-radius: 999px;
|
|
1355
|
+
background: color-mix(in srgb, var(--state-color) 12%, transparent);
|
|
1356
|
+
color: var(--page-ink);
|
|
1357
|
+
font-size: 0.94rem;
|
|
1358
|
+
line-height: 1.5;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
@media (max-width: 520px) {
|
|
1362
|
+
body {
|
|
1363
|
+
padding: 16px;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
main {
|
|
1367
|
+
border-radius: 24px;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.next-step {
|
|
1371
|
+
border-radius: 18px;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
</style>
|
|
1375
|
+
</head>
|
|
1376
|
+
<body>
|
|
1377
|
+
<main data-state="${pageState}" aria-labelledby="callback-title">
|
|
1378
|
+
<div class="mark" aria-hidden="true">
|
|
1379
|
+
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1380
|
+
${stateIcon}
|
|
1381
|
+
</svg>
|
|
1382
|
+
</div>
|
|
1383
|
+
<p class="eyebrow">${eyebrow}</p>
|
|
1384
|
+
<h1 id="callback-title">${escapedCardTitle}</h1>
|
|
1385
|
+
<p class="message">${escapedMessage}</p>
|
|
1386
|
+
<p class="next-step">${actionText}</p>
|
|
1387
|
+
</main>
|
|
1388
|
+
${autoCloseScript}
|
|
1389
|
+
</body>
|
|
1390
|
+
</html>`);
|
|
800
1391
|
};
|
|
801
1392
|
const startLocalCallbackServer = async ({
|
|
802
1393
|
expectedState,
|
|
@@ -861,9 +1452,6 @@ const startLocalCallbackServer = async ({
|
|
|
861
1452
|
state
|
|
862
1453
|
});
|
|
863
1454
|
});
|
|
864
|
-
server.on('error', error => {
|
|
865
|
-
fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
|
|
866
|
-
});
|
|
867
1455
|
const close = async () => {
|
|
868
1456
|
await new Promise((resolve, reject) => {
|
|
869
1457
|
server.close(error => {
|
|
@@ -876,8 +1464,17 @@ const startLocalCallbackServer = async ({
|
|
|
876
1464
|
});
|
|
877
1465
|
};
|
|
878
1466
|
await new Promise((resolve, reject) => {
|
|
879
|
-
|
|
880
|
-
|
|
1467
|
+
const rejectStart = error => {
|
|
1468
|
+
reject(toCallbackServerStartError(error));
|
|
1469
|
+
};
|
|
1470
|
+
server.once('error', rejectStart);
|
|
1471
|
+
server.listen(24953, 'localhost', () => {
|
|
1472
|
+
server.off('error', rejectStart);
|
|
1473
|
+
server.on('error', error => {
|
|
1474
|
+
fail(error instanceof Error ? error : new Error('Local OAuth callback server failed.'));
|
|
1475
|
+
});
|
|
1476
|
+
resolve();
|
|
1477
|
+
});
|
|
881
1478
|
});
|
|
882
1479
|
signal.addEventListener('abort', () => {
|
|
883
1480
|
fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
|
|
@@ -941,7 +1538,7 @@ const fetchPublishedSkillsGraphQl = async ({
|
|
|
941
1538
|
};
|
|
942
1539
|
}
|
|
943
1540
|
if (response.status === 401 || response.status === 403) {
|
|
944
|
-
await
|
|
1541
|
+
await clearAuthState(config.authStatePath);
|
|
945
1542
|
onAuthStateChanged?.();
|
|
946
1543
|
return {
|
|
947
1544
|
ok: false,
|
|
@@ -987,7 +1584,7 @@ const fetchPublishedSkillsGraphQl = async ({
|
|
|
987
1584
|
if (body.errors?.length) {
|
|
988
1585
|
const message = body.errors.map(error => error.message).join('; ');
|
|
989
1586
|
if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
|
|
990
|
-
await
|
|
1587
|
+
await clearAuthState(config.authStatePath);
|
|
991
1588
|
onAuthStateChanged?.();
|
|
992
1589
|
return {
|
|
993
1590
|
ok: false,
|
|
@@ -1260,11 +1857,11 @@ const openBrowser = async url => {
|
|
|
1260
1857
|
const normalizeDirectoryArg = (contextDirectory, directory) => {
|
|
1261
1858
|
return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
|
|
1262
1859
|
};
|
|
1263
|
-
const getDetailCacheKey = (
|
|
1264
|
-
return JSON.stringify([
|
|
1860
|
+
const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
|
|
1861
|
+
return JSON.stringify([catalogCacheKey, skillVersionId]);
|
|
1265
1862
|
};
|
|
1266
|
-
const getDetailInflightKey = (
|
|
1267
|
-
return JSON.stringify([
|
|
1863
|
+
const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
|
|
1864
|
+
return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
|
|
1268
1865
|
};
|
|
1269
1866
|
const OpencodeWizardSkillsPlugin = async input => {
|
|
1270
1867
|
const {
|
|
@@ -1276,7 +1873,6 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1276
1873
|
const catalogInflight = new Map();
|
|
1277
1874
|
const detailCache = new Map();
|
|
1278
1875
|
const detailInflight = new Map();
|
|
1279
|
-
const authStateFile = path.resolve(input.worktree, config.authStatePath);
|
|
1280
1876
|
const initialAuthState = await resolveStoredAuthState(input.worktree, config);
|
|
1281
1877
|
const loginBootstrap = {
|
|
1282
1878
|
promise: null,
|
|
@@ -1375,7 +1971,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1375
1971
|
};
|
|
1376
1972
|
const persistAuthState = async session => {
|
|
1377
1973
|
const authState = toAuthState(session);
|
|
1378
|
-
await writeAuthState(
|
|
1974
|
+
await writeAuthState(config.authStatePath, authState);
|
|
1379
1975
|
clearPublishedSkillState();
|
|
1380
1976
|
return authState;
|
|
1381
1977
|
};
|
|
@@ -1396,19 +1992,20 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1396
1992
|
};
|
|
1397
1993
|
const loginPromise = (async () => {
|
|
1398
1994
|
const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
1399
|
-
|
|
1400
|
-
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1401
|
-
loginBootstrap.snapshot = {
|
|
1402
|
-
status: 'pending',
|
|
1403
|
-
trigger,
|
|
1404
|
-
startedAt,
|
|
1405
|
-
expiresAt: loginStart.expiresAt,
|
|
1406
|
-
browserUrl: loginStart.browserUrl,
|
|
1407
|
-
browserOpenError,
|
|
1408
|
-
email: null,
|
|
1409
|
-
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
|
|
1410
|
-
};
|
|
1995
|
+
let loginStart = null;
|
|
1411
1996
|
try {
|
|
1997
|
+
loginStart = await startLoginFlow(loginSignal);
|
|
1998
|
+
const browserOpenError = await openBrowser(loginStart.browserUrl);
|
|
1999
|
+
loginBootstrap.snapshot = {
|
|
2000
|
+
status: 'pending',
|
|
2001
|
+
trigger,
|
|
2002
|
+
startedAt,
|
|
2003
|
+
expiresAt: loginStart.expiresAt,
|
|
2004
|
+
browserUrl: loginStart.browserUrl,
|
|
2005
|
+
browserOpenError,
|
|
2006
|
+
email: null,
|
|
2007
|
+
message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
|
|
2008
|
+
};
|
|
1412
2009
|
const callbackPayload = await loginStart.callbackPromise;
|
|
1413
2010
|
if (callbackPayload.status === 'error') {
|
|
1414
2011
|
throw new Error(callbackPayload.message);
|
|
@@ -1467,7 +2064,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1467
2064
|
};
|
|
1468
2065
|
throw error;
|
|
1469
2066
|
} finally {
|
|
1470
|
-
await loginStart
|
|
2067
|
+
await loginStart?.closeCallbackServer().catch(() => undefined);
|
|
1471
2068
|
loginBootstrap.promise = null;
|
|
1472
2069
|
}
|
|
1473
2070
|
})();
|
|
@@ -1484,7 +2081,8 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1484
2081
|
directory
|
|
1485
2082
|
});
|
|
1486
2083
|
const directoryPath = workspaceResolution.directoryPath;
|
|
1487
|
-
const
|
|
2084
|
+
const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
|
|
2085
|
+
const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
|
|
1488
2086
|
const cached = cache.get(cacheKey);
|
|
1489
2087
|
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1490
2088
|
return {
|
|
@@ -1527,8 +2125,10 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1527
2125
|
purpose
|
|
1528
2126
|
}) => {
|
|
1529
2127
|
const directoryPath = workspaceResolution.directoryPath;
|
|
1530
|
-
const
|
|
1531
|
-
const
|
|
2128
|
+
const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
|
|
2129
|
+
const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
|
|
2130
|
+
const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
|
|
2131
|
+
const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
|
|
1532
2132
|
const cached = detailCache.get(cacheKey);
|
|
1533
2133
|
if (purpose === 'SYSTEM_CONTEXT' && useCache && cached && cached.expiresAt > Date.now()) {
|
|
1534
2134
|
return {
|
|
@@ -1668,18 +2268,29 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1668
2268
|
loginBootstrapSnapshot: loginBootstrap.snapshot
|
|
1669
2269
|
});
|
|
1670
2270
|
}
|
|
1671
|
-
const
|
|
2271
|
+
const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
2272
|
+
if (!filteredPublishedSkillsResult.fetchResult.ok) {
|
|
2273
|
+
await emitFetchOutcome('FETCH_FAILED');
|
|
2274
|
+
return toFetchFailureOutput({
|
|
2275
|
+
worktree: input.worktree,
|
|
2276
|
+
config,
|
|
2277
|
+
publishedSkillsResult: filteredPublishedSkillsResult,
|
|
2278
|
+
loginBootstrapSnapshot: loginBootstrap.snapshot
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
|
|
1672
2282
|
const isSingleRequest = requestedSkills.length === 1;
|
|
1673
2283
|
if (requestedSkills.length === 0) {
|
|
1674
|
-
const catalog = toPublishedSkillCatalog(
|
|
2284
|
+
const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
|
|
1675
2285
|
context.metadata({
|
|
1676
|
-
title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount}`,
|
|
2286
|
+
title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
|
|
1677
2287
|
metadata: {
|
|
1678
|
-
...toWorkspaceResolutionMetadata(
|
|
2288
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
1679
2289
|
status: 'ready',
|
|
1680
2290
|
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
1681
2291
|
globalAssignmentCount: catalog.assignmentCounts.global.toString(),
|
|
1682
|
-
projectAssignmentCount: catalog.assignmentCounts.project.toString()
|
|
2292
|
+
projectAssignmentCount: catalog.assignmentCounts.project.toString(),
|
|
2293
|
+
ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
|
|
1683
2294
|
}
|
|
1684
2295
|
});
|
|
1685
2296
|
await emitFetchOutcome('FETCH_SUCCESS');
|
|
@@ -1687,16 +2298,21 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1687
2298
|
output: JSON.stringify({
|
|
1688
2299
|
...catalog,
|
|
1689
2300
|
status: 'ready',
|
|
1690
|
-
requestedDirectoryPath:
|
|
1691
|
-
workspaceResolution: toWorkspaceResolutionOutput(
|
|
1692
|
-
|
|
1693
|
-
|
|
2301
|
+
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2302
|
+
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2303
|
+
ignoredPublishedSkills: {
|
|
2304
|
+
scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
|
|
2305
|
+
count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
|
|
2306
|
+
},
|
|
2307
|
+
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2308
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
1694
2309
|
message: 'Catalog discovery only. Provide `skill` or `skills` to fetch markdown bodies/details for selected skills.'
|
|
1695
2310
|
}, null, 2),
|
|
1696
2311
|
metadata: {
|
|
1697
2312
|
status: 'ready',
|
|
1698
|
-
...toWorkspaceResolutionMetadata(
|
|
1699
|
-
publishedSkillCount: catalog.publishedSkillCount.toString()
|
|
2313
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2314
|
+
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
2315
|
+
ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
|
|
1700
2316
|
}
|
|
1701
2317
|
};
|
|
1702
2318
|
}
|
|
@@ -1707,19 +2323,23 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1707
2323
|
pluginId: PLUGIN_ID,
|
|
1708
2324
|
runtimeMode: 'tool_fetch_only',
|
|
1709
2325
|
status: 'not_found',
|
|
1710
|
-
requestedDirectoryPath:
|
|
1711
|
-
workspaceResolution: toWorkspaceResolutionOutput(
|
|
2326
|
+
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2327
|
+
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
1712
2328
|
requestedSkill: requestedSkills[0],
|
|
1713
|
-
availableSkills:
|
|
2329
|
+
availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
|
|
2330
|
+
ignoredPublishedSkills: {
|
|
2331
|
+
scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
|
|
2332
|
+
count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
|
|
2333
|
+
}
|
|
1714
2334
|
}, null, 2),
|
|
1715
2335
|
metadata: {
|
|
1716
2336
|
status: 'not_found',
|
|
1717
|
-
...toWorkspaceResolutionMetadata(
|
|
2337
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
|
|
1718
2338
|
}
|
|
1719
2339
|
};
|
|
1720
2340
|
}
|
|
1721
2341
|
let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
1722
|
-
workspaceResolution:
|
|
2342
|
+
workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
|
|
1723
2343
|
item,
|
|
1724
2344
|
signal: context.abort,
|
|
1725
2345
|
useCache: !args.refresh,
|
|
@@ -1731,7 +2351,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1731
2351
|
await schedulePresenceStart(authState);
|
|
1732
2352
|
});
|
|
1733
2353
|
skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
|
|
1734
|
-
workspaceResolution:
|
|
2354
|
+
workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
|
|
1735
2355
|
item,
|
|
1736
2356
|
signal: context.abort,
|
|
1737
2357
|
useCache: false,
|
|
@@ -1757,7 +2377,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1757
2377
|
context.metadata({
|
|
1758
2378
|
title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
|
|
1759
2379
|
metadata: {
|
|
1760
|
-
...toWorkspaceResolutionMetadata(
|
|
2380
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
1761
2381
|
skillSlug: detail.skillSlug,
|
|
1762
2382
|
version: detail.version
|
|
1763
2383
|
}
|
|
@@ -1767,16 +2387,16 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1767
2387
|
output: JSON.stringify({
|
|
1768
2388
|
pluginId: PLUGIN_ID,
|
|
1769
2389
|
runtimeMode: 'tool_fetch_only',
|
|
1770
|
-
requestedDirectoryPath:
|
|
1771
|
-
workspaceResolution: toWorkspaceResolutionOutput(
|
|
1772
|
-
workspace:
|
|
1773
|
-
fetchedAt:
|
|
1774
|
-
source:
|
|
2390
|
+
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2391
|
+
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2392
|
+
workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
|
|
2393
|
+
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2394
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
1775
2395
|
skill: detail
|
|
1776
2396
|
}, null, 2),
|
|
1777
2397
|
metadata: {
|
|
1778
2398
|
status: 'ready',
|
|
1779
|
-
...toWorkspaceResolutionMetadata(
|
|
2399
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
1780
2400
|
skillSlug: detail.skillSlug
|
|
1781
2401
|
}
|
|
1782
2402
|
};
|
|
@@ -1784,7 +2404,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1784
2404
|
context.metadata({
|
|
1785
2405
|
title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
|
|
1786
2406
|
metadata: {
|
|
1787
|
-
...toWorkspaceResolutionMetadata(
|
|
2407
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
1788
2408
|
requestedCount: requestedSkills.length.toString(),
|
|
1789
2409
|
matchedCount: skillDetails.length.toString()
|
|
1790
2410
|
}
|
|
@@ -1794,18 +2414,18 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1794
2414
|
output: JSON.stringify({
|
|
1795
2415
|
pluginId: PLUGIN_ID,
|
|
1796
2416
|
runtimeMode: 'tool_fetch_only',
|
|
1797
|
-
requestedDirectoryPath:
|
|
1798
|
-
workspaceResolution: toWorkspaceResolutionOutput(
|
|
1799
|
-
workspace:
|
|
1800
|
-
fetchedAt:
|
|
1801
|
-
source:
|
|
2417
|
+
requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
|
|
2418
|
+
workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
|
|
2419
|
+
workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
|
|
2420
|
+
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2421
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
1802
2422
|
requestedSkills,
|
|
1803
2423
|
missingSkills: selection.missingIdentifiers,
|
|
1804
2424
|
skills: skillDetails
|
|
1805
2425
|
}, null, 2),
|
|
1806
2426
|
metadata: {
|
|
1807
2427
|
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
1808
|
-
...toWorkspaceResolutionMetadata(
|
|
2428
|
+
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
1809
2429
|
matchedCount: skillDetails.length.toString()
|
|
1810
2430
|
}
|
|
1811
2431
|
};
|
|
@@ -1840,7 +2460,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1840
2460
|
metadata
|
|
1841
2461
|
});
|
|
1842
2462
|
return {
|
|
1843
|
-
output: JSON.stringify(snapshot, null, 2),
|
|
2463
|
+
output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
|
|
1844
2464
|
metadata
|
|
1845
2465
|
};
|
|
1846
2466
|
};
|
|
@@ -1900,11 +2520,12 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1900
2520
|
return;
|
|
1901
2521
|
}
|
|
1902
2522
|
}
|
|
2523
|
+
const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
|
|
1903
2524
|
const details = await loadSystemNoteDetails({
|
|
1904
|
-
publishedSkillsResult,
|
|
2525
|
+
publishedSkillsResult: filteredPublishedSkillsResult,
|
|
1905
2526
|
signal: AbortSignal.timeout(5_000)
|
|
1906
2527
|
});
|
|
1907
|
-
const systemNote = buildSystemNote(
|
|
2528
|
+
const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, details);
|
|
1908
2529
|
if (!systemNote) return;
|
|
1909
2530
|
output.system.push(systemNote);
|
|
1910
2531
|
}
|