@aexol/opencode-wizard 0.3.7 → 0.3.9

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.
@@ -21,9 +21,34 @@ export { buildSystemNote, resolvePluginStatusSnapshot, toPluginAuthStateSummary,
21
21
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
22
22
  export { resolvePluginStatusSnapshotWithAuthBootstrap } from './auth-bootstrap.js';
23
23
  export { setPublishedSkillIgnored, setPublishedSkillInstalled } from './preferences.js';
24
- const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
25
- return JSON.stringify([catalogCacheKey, skillVersionId]);
24
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId, revision) => {
25
+ return JSON.stringify([catalogCacheKey, skillVersionId, revision]);
26
26
  };
27
+ const getPublishedSkillRevision = item => {
28
+ return item.publishedArtifact.revision ?? `${item.publishedArtifact.checksum}:${item.publishedArtifact.updatedAtCursor ?? item.publishedArtifact.publishedAt}`;
29
+ };
30
+ const getCatalogCursor = (items, catalogItems) => {
31
+ return [...items.map(getPublishedSkillRevision), ...catalogItems.map(item => getPublishedSkillRevision({
32
+ ...item,
33
+ assignmentSource: 'CATALOG',
34
+ assignmentType: 'PATH',
35
+ scopePath: '',
36
+ includeChildren: true
37
+ }))].sort().join('|');
38
+ };
39
+ const getWizardArtifactRevision = item => {
40
+ return item.artifactVersion.revision ?? `${item.artifactVersion.checksum}:${item.artifactVersion.updatedAtCursor ?? item.artifactVersion.publishedAt ?? 'unpublished'}`;
41
+ };
42
+ const getWizardArtifactCatalogCursor = (items, catalogItems) => {
43
+ return [...items.map(getWizardArtifactRevision), ...catalogItems.map(getWizardArtifactRevision)].sort().join('|');
44
+ };
45
+ const toWizardArtifactCatalogCursorItems = items => items.map(item => ({
46
+ ...item,
47
+ assignmentSource: 'CATALOG',
48
+ assignmentType: 'PATH',
49
+ scopePath: '',
50
+ includeChildren: true
51
+ }));
27
52
  const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
28
53
  return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
29
54
  };
@@ -102,7 +127,9 @@ export const OpencodeWizardSkillsPlugin = async input => {
102
127
  const cache = new Map();
103
128
  const catalogInflight = new Map();
104
129
  const detailCache = new Map();
130
+ const wizardArtifactDetailCache = new Map();
105
131
  const detailInflight = new Map();
132
+ const wizardArtifactDetailInflight = new Map();
106
133
  const initialAuthState = await resolveStoredAuthState(input.worktree, config);
107
134
  const loginBootstrap = {
108
135
  promise: null,
@@ -197,7 +224,9 @@ export const OpencodeWizardSkillsPlugin = async input => {
197
224
  cache.clear();
198
225
  catalogInflight.clear();
199
226
  detailCache.clear();
227
+ wizardArtifactDetailCache.clear();
200
228
  detailInflight.clear();
229
+ wizardArtifactDetailInflight.clear();
201
230
  };
202
231
  const persistAuthState = async session => {
203
232
  const authState = toAuthState(session);
@@ -337,7 +366,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
337
366
  });
338
367
  cache.set(cacheKey, {
339
368
  result: fetchResult,
340
- expiresAt: Date.now() + CACHE_TTL_MS
369
+ expiresAt: Date.now() + CACHE_TTL_MS,
370
+ cursor: fetchResult.ok ? getCatalogCursor(fetchResult.payload.skills, fetchResult.payload.catalogSkills) : fetchResult.fetchedAt
341
371
  });
342
372
  return {
343
373
  directoryPath,
@@ -362,7 +392,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
362
392
  const directoryPath = workspaceResolution.directoryPath;
363
393
  const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
364
394
  const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
365
- const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
395
+ const itemRevision = getPublishedSkillRevision(item);
396
+ const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id, itemRevision);
366
397
  const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
367
398
  const cached = detailCache.get(cacheKey);
368
399
  if (useCache && cached && cached.expiresAt > Date.now()) {
@@ -412,7 +443,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
412
443
  }
413
444
  detailCache.set(cacheKey, {
414
445
  artifact: detailResult.artifact,
415
- expiresAt: Date.now() + CACHE_TTL_MS
446
+ expiresAt: Date.now() + CACHE_TTL_MS,
447
+ revision: itemRevision
416
448
  });
417
449
  return {
418
450
  ok: true,
@@ -429,6 +461,62 @@ export const OpencodeWizardSkillsPlugin = async input => {
429
461
  detailInflight.delete(inflightKey);
430
462
  }
431
463
  };
464
+ const loadWizardArtifactDetail = async ({
465
+ workspaceResolution,
466
+ item,
467
+ signal,
468
+ useCache,
469
+ purpose,
470
+ artifactKind
471
+ }) => {
472
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
473
+ const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
474
+ const itemRevision = getWizardArtifactRevision(item);
475
+ const cacheKey = getDetailCacheKey(catalogCacheKey, item.artifactVersion.id, itemRevision);
476
+ const inflightKey = JSON.stringify([catalogCacheKey, item.artifactVersion.id, itemRevision, purpose]);
477
+ const cached = wizardArtifactDetailCache.get(cacheKey);
478
+ if (useCache && cached && cached.expiresAt > Date.now()) {
479
+ return {
480
+ ok: true,
481
+ artifact: cached.artifact
482
+ };
483
+ }
484
+ const inflight = wizardArtifactDetailInflight.get(inflightKey);
485
+ if (useCache && inflight) {
486
+ const inflightResult = await inflight;
487
+ if (!inflightResult.ok) return inflightResult;
488
+ return {
489
+ ok: true,
490
+ artifact: inflightResult.artifact
491
+ };
492
+ }
493
+ const requestPromise = fetchWizardArtifactDetail({
494
+ worktree: input.worktree,
495
+ config,
496
+ resolution: workspaceResolution,
497
+ artifactKind,
498
+ artifactVersionId: item.artifactVersion.id,
499
+ signal,
500
+ onAuthStateChanged: clearPublishedSkillState,
501
+ purpose
502
+ });
503
+ wizardArtifactDetailInflight.set(inflightKey, requestPromise);
504
+ try {
505
+ const detailResult = await requestPromise;
506
+ if (!detailResult.ok) return detailResult;
507
+ wizardArtifactDetailCache.set(cacheKey, {
508
+ artifact: detailResult.artifact,
509
+ expiresAt: Date.now() + CACHE_TTL_MS,
510
+ revision: itemRevision
511
+ });
512
+ return {
513
+ ok: true,
514
+ artifact: detailResult.artifact
515
+ };
516
+ } finally {
517
+ wizardArtifactDetailInflight.delete(inflightKey);
518
+ }
519
+ };
432
520
  const executePublishedSkillsFetchTool = async ({
433
521
  args,
434
522
  context
@@ -520,13 +608,15 @@ export const OpencodeWizardSkillsPlugin = async input => {
520
608
  },
521
609
  fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
522
610
  source: filteredPublishedSkillsResult.fetchResult.source,
611
+ cacheCursor: getCatalogCursor(filteredPublishedSkillsResult.fetchResult.payload.skills, filteredPublishedSkillsResult.fetchResult.payload.catalogSkills),
523
612
  cacheTtlMs: CACHE_TTL_MS,
524
- 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.'
613
+ 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 include deterministic revision cursors; 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.'
525
614
  }, null, 2),
526
615
  metadata: {
527
616
  status: 'ready',
528
617
  ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
529
618
  source: filteredPublishedSkillsResult.fetchResult.source,
619
+ cacheCursor: getCatalogCursor(filteredPublishedSkillsResult.fetchResult.payload.skills, filteredPublishedSkillsResult.fetchResult.payload.catalogSkills),
530
620
  publishedSkillCount: catalog.publishedSkillCount.toString(),
531
621
  globalAssignmentCount: catalog.assignmentCounts.global.toString(),
532
622
  projectAssignmentCount: catalog.assignmentCounts.project.toString(),
@@ -696,6 +786,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
696
786
  pluginId: PLUGIN_ID,
697
787
  availableTools: resolveAvailableTools(authState?.role ?? null)
698
788
  });
789
+ const cacheCursor = getWizardArtifactCatalogCursor(fetchResult.payload.artifacts, toWizardArtifactCatalogCursorItems(fetchResult.payload.catalogArtifacts));
699
790
  return {
700
791
  output: JSON.stringify({
701
792
  ...catalog,
@@ -704,14 +795,18 @@ export const OpencodeWizardSkillsPlugin = async input => {
704
795
  workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
705
796
  fetchedAt: fetchResult.fetchedAt,
706
797
  source: fetchResult.source,
707
- message: 'DESIGN_DOC catalog discovery only. Full DESIGN.md bodies/files require opencode_wizard_artifact_fetch with artifactKind DESIGN_DOC.'
798
+ cacheCursor,
799
+ cacheTtlMs: CACHE_TTL_MS,
800
+ message: 'Generic artifact catalog discovery only. Full bodies/files require opencode_wizard_artifact_fetch with artifactKind and artifact identifiers.'
708
801
  }, null, 2),
709
802
  metadata: {
710
803
  status: 'ready',
711
804
  artifactKind,
712
805
  artifactCount: catalog.artifactCount.toString(),
713
806
  ...toWorkspaceResolutionMetadata(workspaceResolution),
714
- source: fetchResult.source
807
+ source: fetchResult.source,
808
+ cacheCursor,
809
+ cacheTtlMs: CACHE_TTL_MS.toString()
715
810
  }
716
811
  };
717
812
  }
@@ -775,6 +870,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
775
870
  pluginId: PLUGIN_ID,
776
871
  availableTools: resolveAvailableTools(authState?.role ?? null)
777
872
  });
873
+ const cacheCursor = getWizardArtifactCatalogCursor(fetchResult.payload.artifacts, toWizardArtifactCatalogCursorItems(fetchResult.payload.catalogArtifacts));
778
874
  return {
779
875
  output: JSON.stringify({
780
876
  ...catalog,
@@ -783,11 +879,15 @@ export const OpencodeWizardSkillsPlugin = async input => {
783
879
  workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
784
880
  fetchedAt: fetchResult.fetchedAt,
785
881
  source: fetchResult.source,
786
- message: 'Provide artifact or artifacts to fetch DESIGN.md body/files.'
882
+ cacheCursor,
883
+ cacheTtlMs: CACHE_TTL_MS,
884
+ message: 'Provide artifact or artifacts to fetch artifact body/files.'
787
885
  }, null, 2),
788
886
  metadata: {
789
887
  status: 'ready',
790
888
  artifactKind,
889
+ cacheCursor,
890
+ cacheTtlMs: CACHE_TTL_MS.toString(),
791
891
  ...toWorkspaceResolutionMetadata(workspaceResolution)
792
892
  }
793
893
  };
@@ -811,15 +911,13 @@ export const OpencodeWizardSkillsPlugin = async input => {
811
911
  }
812
912
  };
813
913
  }
814
- const detailResults = await Promise.all(selection.selectedItems.map(item => fetchWizardArtifactDetail({
815
- worktree: input.worktree,
816
- config,
817
- resolution: workspaceResolution,
818
- artifactKind,
819
- artifactVersionId: item.artifactVersion.id,
914
+ const detailResults = await Promise.all(selection.selectedItems.map(item => loadWizardArtifactDetail({
915
+ workspaceResolution,
916
+ item,
820
917
  signal: context.abort,
821
- onAuthStateChanged: clearPublishedSkillState,
822
- purpose: 'TOOL_FETCH'
918
+ useCache: !args.refresh,
919
+ purpose: 'TOOL_FETCH',
920
+ artifactKind
823
921
  })));
824
922
  const failedDetail = detailResults.find(result => !result.ok);
825
923
  if (failedDetail && !failedDetail.ok) {
@@ -849,6 +947,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
849
947
  artifactVersion: result.artifact
850
948
  });
851
949
  });
950
+ const cacheCursor = getWizardArtifactCatalogCursor(selection.selectedItems, toWizardArtifactCatalogCursorItems(fetchResult.payload.catalogArtifacts));
852
951
  return {
853
952
  output: JSON.stringify({
854
953
  pluginId: PLUGIN_ID,
@@ -859,6 +958,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
859
958
  workspace: fetchResult.payload.workspace,
860
959
  fetchedAt: fetchResult.fetchedAt,
861
960
  source: fetchResult.source,
961
+ cacheCursor,
962
+ cacheTtlMs: CACHE_TTL_MS,
862
963
  requestedArtifacts,
863
964
  missingArtifacts: selection.missingIdentifiers,
864
965
  artifacts: details
@@ -867,6 +968,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
867
968
  status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
868
969
  artifactKind,
869
970
  matchedCount: details.length.toString(),
971
+ cacheTtlMs: CACHE_TTL_MS.toString(),
870
972
  ...toWorkspaceResolutionMetadata(workspaceResolution)
871
973
  }
872
974
  };
@@ -1139,6 +1241,99 @@ export const OpencodeWizardSkillsPlugin = async input => {
1139
1241
  });
1140
1242
  return withWizardArtifactEnvelope(result, artifactKind);
1141
1243
  };
1244
+ const executeEditorCreateOrUpdateSkillMarkdown = async ({
1245
+ markdownContent,
1246
+ context,
1247
+ source,
1248
+ requestedSkillSlug
1249
+ }) => {
1250
+ const authState = await resolveStoredAuthState(input.worktree, config);
1251
+ if (!authState || authState.role !== 'EDITOR') {
1252
+ return {
1253
+ output: JSON.stringify({
1254
+ pluginId: PLUGIN_ID,
1255
+ status: 'forbidden',
1256
+ skillSlug: requestedSkillSlug ?? null,
1257
+ source,
1258
+ message: 'This tool requires EDITOR role. Your current session does not have the required editor permission.'
1259
+ }, null, 2),
1260
+ metadata: {
1261
+ status: 'forbidden',
1262
+ role: authState?.role ?? 'none'
1263
+ }
1264
+ };
1265
+ }
1266
+ if (!markdownContent.trim()) {
1267
+ throw new Error('Editor skill create/update requires non-empty markdownContent.');
1268
+ }
1269
+ const requestedDirectory = normalizeDirectoryArg(context.directory, undefined);
1270
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1271
+ lastInteractiveDirectoryPath = directoryPath;
1272
+ const response = await fetchPublishedSkillsGraphQl({
1273
+ worktree: input.worktree,
1274
+ config,
1275
+ query: CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION,
1276
+ variables: {
1277
+ markdownContent
1278
+ },
1279
+ signal: context.abort
1280
+ });
1281
+ if (!response.ok) {
1282
+ return {
1283
+ output: JSON.stringify({
1284
+ pluginId: PLUGIN_ID,
1285
+ status: 'request_failed',
1286
+ skillSlug: requestedSkillSlug ?? null,
1287
+ source,
1288
+ message: response.result.message
1289
+ }, null, 2),
1290
+ metadata: {
1291
+ status: 'request_failed',
1292
+ skillSlug: requestedSkillSlug ?? '',
1293
+ directoryPath
1294
+ }
1295
+ };
1296
+ }
1297
+ const payload = response.data.admin.createOrUpdateSkillFromMarkdown;
1298
+ await scheduleInteractivePresenceStart();
1299
+ return {
1300
+ output: JSON.stringify({
1301
+ pluginId: PLUGIN_ID,
1302
+ status: payload.success ? 'created_or_updated' : 'create_or_update_failed',
1303
+ source,
1304
+ skillSlug: payload.skillSlug,
1305
+ skillVersionId: payload.skillVersionId,
1306
+ artifactSlug: payload.artifactSlug,
1307
+ artifactVersionId: payload.artifactVersionId,
1308
+ requestedDirectoryPath: directoryPath,
1309
+ errors: payload.errors,
1310
+ message: source === 'direct_markdown' ? 'Skill markdown was sent directly to the backend; no local seed file was required.' : 'Local seed SKILL.md was published through the backend skill markdown mutation.'
1311
+ }, null, 2),
1312
+ metadata: {
1313
+ status: payload.success ? 'created_or_updated' : 'create_or_update_failed',
1314
+ skillSlug: payload.skillSlug,
1315
+ skillVersionId: payload.skillVersionId ?? '',
1316
+ source,
1317
+ directoryPath
1318
+ }
1319
+ };
1320
+ };
1321
+ const executeEditorCreateOrUpdateSkillTool = async ({
1322
+ args,
1323
+ context
1324
+ }) => {
1325
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1326
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1327
+ lastInteractiveDirectoryPath = directoryPath;
1328
+ return executeEditorCreateOrUpdateSkillMarkdown({
1329
+ markdownContent: args.markdownContent,
1330
+ context: {
1331
+ ...context,
1332
+ directory: requestedDirectory
1333
+ },
1334
+ source: 'direct_markdown'
1335
+ });
1336
+ };
1142
1337
  const executeEditorPublishSkillTool = async ({
1143
1338
  args,
1144
1339
  context
@@ -1154,6 +1349,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
1154
1349
  pluginId: PLUGIN_ID,
1155
1350
  status: 'forbidden',
1156
1351
  skillSlug: requestedSkillSlug,
1352
+ source: 'seed_file',
1157
1353
  message: 'This tool requires EDITOR role. Your current session does not have the required editor permission.'
1158
1354
  }, null, 2),
1159
1355
  metadata: {
@@ -1191,47 +1387,15 @@ export const OpencodeWizardSkillsPlugin = async input => {
1191
1387
  }
1192
1388
  };
1193
1389
  }
1194
- const response = await fetchPublishedSkillsGraphQl({
1195
- worktree: input.worktree,
1196
- config,
1197
- query: CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION,
1198
- variables: {
1199
- markdownContent
1390
+ return executeEditorCreateOrUpdateSkillMarkdown({
1391
+ markdownContent,
1392
+ context: {
1393
+ ...context,
1394
+ directory: requestedDirectory
1200
1395
  },
1201
- signal: context.abort
1396
+ source: 'seed_file',
1397
+ requestedSkillSlug
1202
1398
  });
1203
- if (!response.ok) {
1204
- return {
1205
- output: JSON.stringify({
1206
- pluginId: PLUGIN_ID,
1207
- status: 'request_failed',
1208
- skillSlug: requestedSkillSlug,
1209
- message: response.result.message
1210
- }, null, 2),
1211
- metadata: {
1212
- status: 'request_failed',
1213
- skillSlug: requestedSkillSlug,
1214
- directoryPath
1215
- }
1216
- };
1217
- }
1218
- const payload = response.data.createOrUpdateSkillFromMarkdown;
1219
- await scheduleInteractivePresenceStart();
1220
- return {
1221
- output: JSON.stringify({
1222
- pluginId: PLUGIN_ID,
1223
- status: payload.success ? 'published' : 'publish_failed',
1224
- skillSlug: payload.skillSlug,
1225
- skillVersionId: payload.skillVersionId,
1226
- errors: payload.errors
1227
- }, null, 2),
1228
- metadata: {
1229
- status: payload.success ? 'published' : 'publish_failed',
1230
- skillSlug: payload.skillSlug,
1231
- skillVersionId: payload.skillVersionId ?? '',
1232
- directoryPath
1233
- }
1234
- };
1235
1399
  };
1236
1400
  const executeWizardArtifactImportTool = async ({
1237
1401
  args,
@@ -1348,8 +1512,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
1348
1512
  }
1349
1513
  };
1350
1514
  };
1351
- const role = initialAuthState?.role ?? null;
1352
- const isEditor = role === 'EDITOR';
1353
1515
  const {
1354
1516
  sharedTools,
1355
1517
  editorOnlyTools
@@ -1378,6 +1540,10 @@ export const OpencodeWizardSkillsPlugin = async input => {
1378
1540
  args,
1379
1541
  context
1380
1542
  }),
1543
+ createOrUpdateEditorSkill: (args, context) => executeEditorCreateOrUpdateSkillTool({
1544
+ args,
1545
+ context
1546
+ }),
1381
1547
  publishEditorSkill: (args, context) => executeEditorPublishSkillTool({
1382
1548
  args,
1383
1549
  context
@@ -1388,10 +1554,10 @@ export const OpencodeWizardSkillsPlugin = async input => {
1388
1554
  })
1389
1555
  });
1390
1556
  return {
1391
- tool: {
1557
+ tool: initialAuthState?.role === 'EDITOR' ? {
1392
1558
  ...sharedTools,
1393
- ...(isEditor ? editorOnlyTools : {})
1394
- },
1559
+ ...editorOnlyTools
1560
+ } : sharedTools,
1395
1561
  'experimental.chat.system.transform': async (_hookInput, output) => {
1396
1562
  let publishedSkillsResult = await loadPublishedSkillCatalog({
1397
1563
  directory: input.directory,