@aexol/opencode-wizard 0.3.2 → 0.3.3
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 +4 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +131 -9
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,10 @@ Call `opencode_wizard_published_skills_fetch` without `skill` or `skills` to man
|
|
|
52
52
|
|
|
53
53
|
No-arg discovery returns published skill summaries, assignment counts split into `global`, `project`, `user`, and `other`, policy metadata, and no markdown bodies. Existing `skill` and `skills` calls still fetch one or more full skill body/detail payloads by slug, artifact name, or skill name.
|
|
54
54
|
|
|
55
|
+
Workspace delivery still follows the backend contract: the plugin sends `workspaceSlug` when it has one, otherwise falls back to `repositoryUrl`. The plugin now prefers configured or learned workspace slug mappings over a repo-basename fallback, and learns durable slug-to-repo mappings from successful backend catalog responses so worktrees and nontrivial repo roots stay aligned.
|
|
56
|
+
|
|
57
|
+
Published skill fetches still support `refresh: true`, but normal cache entries now self-expire after 30 seconds and fetch/status payloads surface `source`, `workspaceSlug`, and `workspaceSlugSource` so stale-vs-refreshed behavior is visible without relying on manual cache deletion.
|
|
58
|
+
|
|
55
59
|
Use `opencode_wizard_published_skill_preference_set` for non-TUI preference actions (`install`, `uninstall`, `ignore`, `unignore`) against the same server-backed preference API used by the TUI overlay.
|
|
56
60
|
|
|
57
61
|
`GLOBAL_CONTEXT` skills are active context skills and are not meant to be installed per project. `PROJECT_INSTALLABLE` skills are gallery/installable skills that may be attached globally or to a workspace/path through assignment records; those assignments remain the source of truth for what is active in a catalog response.
|
package/dist/server.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type ResolvedConfig = {
|
|
|
5
5
|
authSessionUrl: string;
|
|
6
6
|
presenceUrl: string;
|
|
7
7
|
actionsUrl: string;
|
|
8
|
+
configuredWorkspaceSlug: string | null;
|
|
8
9
|
fallbackWorkspaceSlug: string;
|
|
9
10
|
rootSkillSeedPath: string;
|
|
10
11
|
authStatePath: string;
|
|
@@ -13,6 +14,8 @@ type WorkspaceResolution = {
|
|
|
13
14
|
requestedDirectory: string;
|
|
14
15
|
repositoryRoot: string;
|
|
15
16
|
repositoryUrl: string | null;
|
|
17
|
+
workspaceSlug?: string | null;
|
|
18
|
+
workspaceSlugSource?: 'configured' | 'learned' | 'backend' | 'fallback' | 'repositoryUrl' | 'placeholder';
|
|
16
19
|
fallbackWorkspaceSlug: string | null;
|
|
17
20
|
directoryPath: string;
|
|
18
21
|
cacheKey: string;
|
|
@@ -271,6 +274,8 @@ declare const toWorkspaceResolutionOutput: (resolution: WorkspaceResolution) =>
|
|
|
271
274
|
requestedDirectory: string;
|
|
272
275
|
repositoryRoot: string;
|
|
273
276
|
repositoryUrl: string | null;
|
|
277
|
+
workspaceSlug: string | null | undefined;
|
|
278
|
+
workspaceSlugSource: "configured" | "learned" | "backend" | "fallback" | "repositoryUrl" | "placeholder" | undefined;
|
|
274
279
|
fallbackWorkspaceSlug: string | null;
|
|
275
280
|
directoryPath: string;
|
|
276
281
|
};
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,7 @@ const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
|
|
|
14
14
|
const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
|
|
15
15
|
export const PLUGIN_ID = 'opencode-wizard';
|
|
16
16
|
const CACHE_TTL_MS = 30_000;
|
|
17
|
+
const WORKSPACE_MAPPING_LIMIT = 100;
|
|
17
18
|
const ROOT_SKILL_SEED_PATH = '.opencode/skills';
|
|
18
19
|
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
|
|
19
20
|
const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
|
|
@@ -330,14 +331,19 @@ const resolveBackendOrigin = async worktree => {
|
|
|
330
331
|
localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
|
|
331
332
|
});
|
|
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
|
+
};
|
|
333
339
|
const toWorkspaceSlug = value => {
|
|
334
340
|
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
335
341
|
if (normalized) return normalized;
|
|
336
342
|
return 'workspace';
|
|
337
343
|
};
|
|
338
344
|
const resolveFallbackWorkspaceSlug = worktree => {
|
|
339
|
-
const configuredWorkspaceSlug =
|
|
340
|
-
if (configuredWorkspaceSlug) return
|
|
345
|
+
const configuredWorkspaceSlug = readConfiguredWorkspaceSlug();
|
|
346
|
+
if (configuredWorkspaceSlug) return configuredWorkspaceSlug;
|
|
341
347
|
return toWorkspaceSlug(path.basename(path.resolve(worktree)));
|
|
342
348
|
};
|
|
343
349
|
export const resolveConfig = async worktree => {
|
|
@@ -348,6 +354,7 @@ export const resolveConfig = async worktree => {
|
|
|
348
354
|
authSessionUrl: `${backendOrigin}/api/opencode-plugin/oauth/session`,
|
|
349
355
|
presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
|
|
350
356
|
actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
|
|
357
|
+
configuredWorkspaceSlug: readConfiguredWorkspaceSlug(),
|
|
351
358
|
fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
|
|
352
359
|
rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
|
|
353
360
|
authStatePath: GLOBAL_CONFIG_PATH
|
|
@@ -408,22 +415,31 @@ const resolveWorkspace = async ({
|
|
|
408
415
|
const gitRoot = await resolveGitRoot(requestedDirectory);
|
|
409
416
|
const repositoryRoot = gitRoot ?? requestedDirectory;
|
|
410
417
|
const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
|
|
418
|
+
const learnedWorkspaceMapping = await findWorkspaceSlugMapping({
|
|
419
|
+
configFile: config.authStatePath,
|
|
420
|
+
repositoryUrl,
|
|
421
|
+
repositoryRoot
|
|
422
|
+
});
|
|
411
423
|
const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
|
|
412
424
|
const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
|
|
413
|
-
const
|
|
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';
|
|
414
428
|
return {
|
|
415
429
|
requestedDirectory,
|
|
416
430
|
repositoryRoot,
|
|
417
431
|
repositoryUrl,
|
|
432
|
+
workspaceSlug,
|
|
433
|
+
workspaceSlugSource,
|
|
418
434
|
fallbackWorkspaceSlug,
|
|
419
435
|
directoryPath,
|
|
420
436
|
cacheKey: JSON.stringify([workspaceIdentity, directoryPath])
|
|
421
437
|
};
|
|
422
438
|
};
|
|
423
439
|
const toDeliveryInput = resolution => {
|
|
424
|
-
if (resolution.
|
|
440
|
+
if (resolution.workspaceSlug) {
|
|
425
441
|
return {
|
|
426
|
-
workspaceSlug: resolution.
|
|
442
|
+
workspaceSlug: resolution.workspaceSlug,
|
|
427
443
|
directoryPath: resolution.directoryPath
|
|
428
444
|
};
|
|
429
445
|
}
|
|
@@ -475,6 +491,76 @@ const withoutLegacyPublishedSkillPreferences = config => {
|
|
|
475
491
|
const hasLegacyPublishedSkillPreferences = config => {
|
|
476
492
|
return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
|
|
477
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
|
+
};
|
|
478
564
|
const readGlobalAuthState = async configFile => {
|
|
479
565
|
const storedConfig = await readGlobalConfig(configFile);
|
|
480
566
|
const storedAuthState = storedConfig.auth;
|
|
@@ -531,7 +617,7 @@ const toPublishedSkillPreferenceScope = (value, defaultScope) => {
|
|
|
531
617
|
throw new Error('Published skill preferenceScope must be either global or project.');
|
|
532
618
|
};
|
|
533
619
|
const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
|
|
534
|
-
const workspaceSlug = payload?.workspace?.slug ?? resolution.fallbackWorkspaceSlug;
|
|
620
|
+
const workspaceSlug = payload?.workspace?.slug ?? resolution.workspaceSlug ?? resolution.fallbackWorkspaceSlug;
|
|
535
621
|
if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
|
|
536
622
|
if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
|
|
537
623
|
return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
|
|
@@ -879,6 +965,8 @@ const toWorkspaceResolutionOutput = resolution => ({
|
|
|
879
965
|
requestedDirectory: resolution.requestedDirectory,
|
|
880
966
|
repositoryRoot: resolution.repositoryRoot,
|
|
881
967
|
repositoryUrl: resolution.repositoryUrl,
|
|
968
|
+
workspaceSlug: resolution.workspaceSlug,
|
|
969
|
+
workspaceSlugSource: resolution.workspaceSlugSource,
|
|
882
970
|
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
|
|
883
971
|
directoryPath: resolution.directoryPath
|
|
884
972
|
});
|
|
@@ -886,6 +974,8 @@ const toWorkspaceResolutionMetadata = resolution => ({
|
|
|
886
974
|
directoryPath: resolution.directoryPath,
|
|
887
975
|
repositoryRoot: resolution.repositoryRoot,
|
|
888
976
|
repositoryUrl: resolution.repositoryUrl ?? '',
|
|
977
|
+
workspaceSlug: resolution.workspaceSlug ?? '',
|
|
978
|
+
workspaceSlugSource: resolution.workspaceSlugSource ?? 'placeholder',
|
|
889
979
|
fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? ''
|
|
890
980
|
});
|
|
891
981
|
const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
|
|
@@ -1183,7 +1273,12 @@ const toPluginStatusMetadata = snapshot => ({
|
|
|
1183
1273
|
pluginStatus: snapshot.status,
|
|
1184
1274
|
authStatus: snapshot.authState.status,
|
|
1185
1275
|
authEmail: snapshot.authState.email ?? '',
|
|
1186
|
-
authUserId: snapshot.authState.userId ?? ''
|
|
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'
|
|
1187
1282
|
});
|
|
1188
1283
|
const isUnauthorizedGraphQlMessage = message => {
|
|
1189
1284
|
const normalizedMessage = message.toLowerCase();
|
|
@@ -1504,6 +1599,24 @@ const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal,
|
|
|
1504
1599
|
source: 'network'
|
|
1505
1600
|
};
|
|
1506
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
|
+
};
|
|
1507
1620
|
const fetchPublishedSkillDetail = async ({
|
|
1508
1621
|
worktree,
|
|
1509
1622
|
config,
|
|
@@ -1941,6 +2054,11 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1941
2054
|
}
|
|
1942
2055
|
const requestPromise = (async () => {
|
|
1943
2056
|
const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
|
|
2057
|
+
await maybePersistWorkspaceSlugFromCatalog({
|
|
2058
|
+
config,
|
|
2059
|
+
resolution: workspaceResolution,
|
|
2060
|
+
fetchResult
|
|
2061
|
+
});
|
|
1944
2062
|
cache.set(cacheKey, {
|
|
1945
2063
|
result: fetchResult,
|
|
1946
2064
|
expiresAt: Date.now() + CACHE_TTL_MS
|
|
@@ -1971,7 +2089,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
1971
2089
|
const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
|
|
1972
2090
|
const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
|
|
1973
2091
|
const cached = detailCache.get(cacheKey);
|
|
1974
|
-
if (
|
|
2092
|
+
if (useCache && cached && cached.expiresAt > Date.now()) {
|
|
1975
2093
|
return {
|
|
1976
2094
|
ok: true,
|
|
1977
2095
|
detail: toPublishedSkillDetail({
|
|
@@ -2148,11 +2266,13 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
2148
2266
|
},
|
|
2149
2267
|
fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
|
|
2150
2268
|
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2151
|
-
|
|
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.'
|
|
2152
2271
|
}, null, 2),
|
|
2153
2272
|
metadata: {
|
|
2154
2273
|
status: 'ready',
|
|
2155
2274
|
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2275
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2156
2276
|
publishedSkillCount: catalog.publishedSkillCount.toString(),
|
|
2157
2277
|
globalAssignmentCount: catalog.assignmentCounts.global.toString(),
|
|
2158
2278
|
projectAssignmentCount: catalog.assignmentCounts.project.toString(),
|
|
@@ -2242,6 +2362,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
2242
2362
|
metadata: {
|
|
2243
2363
|
status: 'ready',
|
|
2244
2364
|
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2365
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2245
2366
|
skillSlug: detail.skillSlug
|
|
2246
2367
|
}
|
|
2247
2368
|
};
|
|
@@ -2271,6 +2392,7 @@ const OpencodeWizardSkillsPlugin = async input => {
|
|
|
2271
2392
|
metadata: {
|
|
2272
2393
|
status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
|
|
2273
2394
|
...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
|
|
2395
|
+
source: filteredPublishedSkillsResult.fetchResult.source,
|
|
2274
2396
|
matchedCount: skillDetails.length.toString()
|
|
2275
2397
|
}
|
|
2276
2398
|
};
|