dsh-agent-plugins-market 0.6.2 → 0.7.1

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.
Files changed (155) hide show
  1. package/README.md +117 -111
  2. package/README.zh.md +112 -98
  3. package/client/client.js +28533 -1511
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +188 -0
  6. package/lib/application/catalog.js +229 -1109
  7. package/lib/application/details.js +19 -8
  8. package/lib/application/install-store.js +95 -0
  9. package/lib/application/lsp-service.js +97 -0
  10. package/lib/application/mcp-service.js +263 -0
  11. package/lib/application/panel-resources.js +43 -14
  12. package/lib/application/ports.js +21 -0
  13. package/lib/application/project-agent-roles.js +8 -2
  14. package/lib/application/snapshot-cache.js +160 -0
  15. package/lib/application/source-store.js +521 -0
  16. package/lib/catalog/archive.js +35 -30
  17. package/lib/catalog/component-files.js +14 -6
  18. package/lib/catalog/fs-probes.js +43 -0
  19. package/lib/catalog/manifests.js +53 -92
  20. package/lib/catalog/native-project.js +2 -2
  21. package/lib/catalog/paths.js +74 -55
  22. package/lib/catalog/plugin-variables.js +38 -0
  23. package/lib/catalog/project-config.js +2 -1
  24. package/lib/catalog/project-hooks.js +23 -2
  25. package/lib/catalog/scan-resolvers.js +29 -45
  26. package/lib/catalog/skills-parse.js +61 -10
  27. package/lib/catalog/source-catalog.js +10 -4
  28. package/lib/catalog/suite-components.js +7 -7
  29. package/lib/catalog/suite-scanner.js +3 -1
  30. package/lib/catalog/surfaces.js +5 -26
  31. package/lib/catalog/validate.js +7 -11
  32. package/lib/contracts/market.js +2 -0
  33. package/lib/contracts/settings.js +63 -0
  34. package/lib/index.js +115 -217
  35. package/lib/model/layouts.js +10 -1
  36. package/lib/model/types.js +26 -3
  37. package/lib/routes.js +57 -15
  38. package/lib/runtime/agent-role-names.js +70 -0
  39. package/lib/runtime/agent-role-router.js +142 -106
  40. package/lib/runtime/commands-mounts.js +55 -11
  41. package/lib/runtime/deadline.js +54 -0
  42. package/lib/runtime/dynamic-context.js +140 -0
  43. package/lib/runtime/feedback-tool.js +101 -46
  44. package/lib/runtime/hooks-mounts.js +9 -11
  45. package/lib/runtime/host-locale.js +49 -24
  46. package/lib/runtime/lsp-direct-config.js +19 -19
  47. package/lib/runtime/lsp-mounts.js +182 -70
  48. package/lib/runtime/lsp-server-state.js +4 -4
  49. package/lib/runtime/lsp-status.js +1 -1
  50. package/lib/runtime/mcp-auth-record.js +42 -0
  51. package/lib/runtime/mcp-backend.js +14 -14
  52. package/lib/runtime/mcp-client/config.js +8 -1
  53. package/lib/runtime/mcp-client/connection.js +38 -43
  54. package/lib/runtime/mcp-client/host-contract.js +10 -0
  55. package/lib/runtime/mcp-client/host-seams.js +8 -88
  56. package/lib/runtime/mcp-client/oauth.js +5 -7
  57. package/lib/runtime/mcp-client/plugin-identity.js +28 -0
  58. package/lib/runtime/mcp-client/projection.js +202 -0
  59. package/lib/runtime/mcp-client/tools.js +4 -189
  60. package/lib/runtime/mcp-config.js +36 -14
  61. package/lib/runtime/mcp-direct-config.js +33 -16
  62. package/lib/runtime/mcp-mounts.js +35 -75
  63. package/lib/runtime/mcp-overrides.js +5 -5
  64. package/lib/runtime/mcp-redaction.js +3 -2
  65. package/lib/runtime/mcp-status.js +7 -8
  66. package/lib/runtime/mount-lifecycle.js +73 -0
  67. package/lib/runtime/profile-seam.js +321 -0
  68. package/lib/runtime/project-runtime.js +34 -17
  69. package/lib/runtime/reconcile-scheduler.js +97 -0
  70. package/lib/runtime/reconciler.js +6 -1
  71. package/lib/runtime/regions.js +0 -19
  72. package/lib/runtime/server-config.js +6 -4
  73. package/lib/runtime/settings-namespace.js +144 -0
  74. package/lib/runtime/skills-provider.js +38 -14
  75. package/lib/runtime/source-auto-update.js +66 -0
  76. package/lib/{model/state.js → runtime/state-store.js} +9 -11
  77. package/lib/runtime/storage-migration.js +35 -18
  78. package/lib/runtime/subagent-catalog.js +5 -4
  79. package/lib/runtime/timer-seat.js +80 -0
  80. package/lib/runtime/tool-registry-observer.js +20 -20
  81. package/lib/runtime/user-commands.js +7 -3
  82. package/lib/runtime/user-panels.js +125 -46
  83. package/lib/runtime/user-store.js +124 -69
  84. package/lib/types/application/catalog-context.d.ts +124 -0
  85. package/lib/types/application/catalog.d.ts +108 -304
  86. package/lib/types/application/install-store.d.ts +21 -0
  87. package/lib/types/application/lsp-service.d.ts +36 -0
  88. package/lib/types/application/mcp-service.d.ts +77 -0
  89. package/lib/types/application/panel-resources.d.ts +8 -0
  90. package/lib/types/application/ports.d.ts +72 -0
  91. package/lib/types/application/queries.d.ts +15 -26
  92. package/lib/types/application/snapshot-cache.d.ts +51 -0
  93. package/lib/types/application/source-store.d.ts +100 -0
  94. package/lib/types/catalog/archive.d.ts +3 -11
  95. package/lib/types/catalog/component-files.d.ts +4 -0
  96. package/lib/types/catalog/fs-probes.d.ts +10 -0
  97. package/lib/types/catalog/manifests.d.ts +17 -16
  98. package/lib/types/catalog/native-project.d.ts +2 -2
  99. package/lib/types/catalog/paths.d.ts +53 -15
  100. package/lib/types/catalog/plugin-variables.d.ts +16 -0
  101. package/lib/types/catalog/scan-pipeline.d.ts +3 -3
  102. package/lib/types/catalog/scan-resolvers.d.ts +3 -9
  103. package/lib/types/catalog/skills-parse.d.ts +13 -7
  104. package/lib/types/catalog/source-catalog.d.ts +10 -5
  105. package/lib/types/catalog/suite-scanner.d.ts +8 -6
  106. package/lib/types/catalog/validate.d.ts +0 -2
  107. package/lib/types/contracts/lsp-status.d.ts +35 -0
  108. package/lib/types/contracts/market.d.ts +13 -2
  109. package/lib/types/contracts/mcp-status.d.ts +4 -0
  110. package/lib/types/contracts/settings.d.ts +59 -0
  111. package/lib/types/model/layouts.d.ts +10 -0
  112. package/lib/types/model/types.d.ts +29 -5
  113. package/lib/types/runtime/agent-role-names.d.ts +6 -0
  114. package/lib/types/runtime/agent-role-router.d.ts +59 -52
  115. package/lib/types/runtime/commands-mounts.d.ts +15 -7
  116. package/lib/types/runtime/deadline.d.ts +17 -0
  117. package/lib/types/runtime/dynamic-context.d.ts +81 -0
  118. package/lib/types/runtime/feedback-tool.d.ts +33 -11
  119. package/lib/types/runtime/hooks-mounts.d.ts +1 -1
  120. package/lib/types/runtime/host-locale.d.ts +20 -2
  121. package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
  122. package/lib/types/runtime/lsp-mounts.d.ts +69 -19
  123. package/lib/types/runtime/lsp-status.d.ts +2 -1
  124. package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
  125. package/lib/types/runtime/mcp-backend.d.ts +10 -14
  126. package/lib/types/runtime/mcp-client/config.d.ts +8 -2
  127. package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
  128. package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
  129. package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
  130. package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
  131. package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
  132. package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
  133. package/lib/types/runtime/mcp-config.d.ts +25 -4
  134. package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
  135. package/lib/types/runtime/mcp-mounts.d.ts +10 -15
  136. package/lib/types/runtime/mcp-overrides.d.ts +1 -1
  137. package/lib/types/runtime/mcp-status.d.ts +1 -1
  138. package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
  139. package/lib/types/runtime/profile-seam.d.ts +64 -0
  140. package/lib/types/runtime/project-runtime.d.ts +10 -3
  141. package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
  142. package/lib/types/runtime/reconciler.d.ts +2 -0
  143. package/lib/types/runtime/regions.d.ts +1 -4
  144. package/lib/types/runtime/settings-namespace.d.ts +76 -0
  145. package/lib/types/runtime/skills-provider.d.ts +11 -2
  146. package/lib/types/runtime/source-auto-update.d.ts +32 -0
  147. package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
  148. package/lib/types/runtime/storage-migration.d.ts +5 -3
  149. package/lib/types/runtime/subagent-catalog.d.ts +2 -0
  150. package/lib/types/runtime/timer-seat.d.ts +51 -0
  151. package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
  152. package/lib/types/runtime/user-panels.d.ts +69 -19
  153. package/lib/types/runtime/user-store.d.ts +67 -20
  154. package/package.json +68 -24
  155. package/schemas/README.md +1 -1
package/lib/index.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { Catalog } from './application/catalog.js';
2
+ import { settlesWithin } from './runtime/deadline.js';
2
3
  import { RuntimeReconciler } from './runtime/reconciler.js';
3
- import { inspectToolRegistry } from './runtime/tool-registry-observer.js';
4
+ import { ReconcileScheduler } from './runtime/reconcile-scheduler.js';
5
+ import { MarketSettingsNamespace } from './runtime/settings-namespace.js';
6
+ import { deleteMcpAuthGrant } from './runtime/mcp-auth-record.js';
7
+ import { inspectToolRegistry, toolsServiceOf } from './runtime/tool-registry-observer.js';
4
8
  import { migratePluginStorage } from './runtime/storage-migration.js';
5
9
  import { mountAgentRoleTool } from './runtime/agent-role-router.js';
6
10
  import { projectAgentRoles } from './application/project-agent-roles.js';
7
11
  import { mountProjectCommands, mountProjectMcp, mountProjectHooks, mountSuiteInstructions } from './runtime/project-runtime.js';
8
12
  import { createPanelResources } from './application/panel-resources.js';
9
- import { resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
13
+ import { resolveAgentsRoot, resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
10
14
  import { mountSuiteRoutes } from './routes.js';
11
- import { MCP_SETTINGS_NAMESPACE, MarketSettingsSchema, readMcpBackend } from './runtime/mcp-backend.js';
12
- import { narrowDownloadRegion } from './runtime/regions.js';
13
15
  import { SuiteSkillProvider } from './runtime/skills-provider.js';
16
+ import { shellSeamOf } from './runtime/dynamic-context.js';
14
17
  import { loadLspServers } from './runtime/lsp-direct-config.js';
15
18
  import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
16
- import { bindHostLocale, loadHostLocale } from './runtime/host-locale.js';
17
- import { mountFeedbackTool } from './runtime/feedback-tool.js';
19
+ import { bindHostLocale, loadHostLocale, readHostLocalePreference, setHostLocaleSource } from './runtime/host-locale.js';
18
20
  import { createUserPanelStores } from './runtime/user-panels.js';
21
+ import { SourceAutoUpdater } from './runtime/source-auto-update.js';
19
22
  import { UserPanelSkillProvider } from './runtime/user-panels.js';
20
23
  import { UserCommandMountRegistry } from './runtime/user-commands.js';
21
24
  export const name = 'dsh-agent-plugins-market';
@@ -23,6 +26,7 @@ export const inject = ['skills', 'commands'];
23
26
  export async function apply(ctx, config = {}) {
24
27
  const userRoot = resolveUserRoot(config.userRoot);
25
28
  const dataRoot = resolveDataRoot(config.dataRoot, userRoot);
29
+ const agentsRoot = resolveAgentsRoot();
26
30
  const migration = await migratePluginStorage(config);
27
31
  if (migration.conflicts.length > 0)
28
32
  throw new Error(`Plugin storage migration conflicts (original files retained): ${migration.conflicts.join(', ')}`);
@@ -30,251 +34,151 @@ export async function apply(ctx, config = {}) {
30
34
  let userPanelControl;
31
35
  // Host runtime copy resolves from the harness `locale.preference` setting;
32
36
  // the async settings read lands before the first session starts in practice.
33
- const t = bindHostLocale(undefined);
34
- void loadHostLocale().then(locale => {
35
- hostLocale.t = locale.t;
36
- providerControl?.invalidate();
37
- userPanelControl?.invalidate();
37
+ const hostLocale = { t: bindHostLocale(undefined) };
38
+ const refreshHostLocale = () => {
39
+ void loadHostLocale().then(locale => {
40
+ hostLocale.t = locale.t;
41
+ providerControl?.invalidate();
42
+ userPanelControl?.invalidate();
43
+ });
44
+ };
45
+ void refreshHostLocale();
46
+ // The locale preference the GUI writes is owned by the host settings service:
47
+ // read it there rather than re-parsing the document, and re-bind once the
48
+ // service lands (the locale namespace may register after this plugin).
49
+ ctx.inject(['settings'], settingsCtx => {
50
+ setHostLocaleSource(() => readHostLocalePreference(settingsCtx));
51
+ refreshHostLocale();
38
52
  });
39
- const hostLocale = { t };
40
- /** Settings watchers to release at teardown (backend watch + feedback switch). */
41
- const settingsWatchers = [];
42
53
  const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
43
54
  // User panel stores (skills / commands / agent personas) and their runtime
44
55
  // contributions: one extra skill provider plus one command mount registry.
45
- const panels = createUserPanelStores(userRoot);
56
+ const panels = createUserPanelStores(agentsRoot);
46
57
  const userCommands = new UserCommandMountRegistry(ctx, panels.commands, key => hostLocale.t(key));
47
58
  let disposed = false;
48
59
  let projectCommands;
49
60
  let projectMcp;
50
61
  let projectHooks;
51
62
  let suitePrompts;
52
- let reconcileRequested = false;
53
- let reconciliation;
54
- const reconcileOnce = async () => {
55
- try {
56
- const suites = await catalog.enabledUserSuites();
57
- if (disposed)
58
- return;
59
- const diagnostics = await runtime.reconcile(suites);
60
- catalog.mcpDiagnostics = diagnostics.mcp;
61
- for (const diagnostic of diagnostics.mcp) {
62
- ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" mcp server "${diagnostic.serverKey}": ${diagnostic.reason}`);
63
- }
64
- for (const diagnostic of diagnostics.lsp) {
65
- ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" lsp server "${diagnostic.serverKey}": ${diagnostic.reason}`);
66
- }
67
- for (const diagnostic of diagnostics.commands) {
68
- if (diagnostic.reason !== '')
69
- ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" command "${diagnostic.command}": ${diagnostic.reason}`);
70
- }
71
- for (const diagnostic of diagnostics.hooks) {
72
- ctx.logger?.warn(`[dsh-agent-plugins-market] suite "${diagnostic.suiteId}" hooks: ${diagnostic.reason}`);
73
- }
74
- for (const error of diagnostics.errors) {
75
- ctx.logger?.warn(`[dsh-agent-plugins-market] ${error.surface} reconcile failed: ${error.reason}`);
76
- }
63
+ const scheduler = new ReconcileScheduler(ctx, runtime, {
64
+ enabledSuites: () => catalog.enabledUserSuites(),
65
+ publishMcpDiagnostics: diagnostics => {
66
+ catalog.mcpDiagnostics = diagnostics;
77
67
  }
78
- catch (error) {
79
- ctx.logger?.warn(`[dsh-agent-plugins-market] runtime reconcile failed: ${error instanceof Error ? error.message : String(error)}`);
80
- throw error;
68
+ });
69
+ /** Reconcile the user command mounts and report each failure once. */
70
+ const reconcileUserCommands = async () => {
71
+ const diagnostics = await userCommands.reconcile().catch(() => []);
72
+ for (const reason of diagnostics) {
73
+ ctx.logger?.warn(`[dsh-agent-plugins-market] user commands: ${reason}`);
81
74
  }
82
75
  };
83
- // Settings, credential and catalog events can arrive during a slow MCP
84
- // connection. Keep one current pass and one fresh follow-up, not a queue
85
- // of stale snapshots that all reconnect the same servers.
86
- const reconcileMounts = () => {
87
- if (disposed)
88
- return Promise.resolve();
89
- reconcileRequested = true;
90
- reconciliation ??= (async () => {
91
- try {
92
- do {
93
- reconcileRequested = false;
94
- await reconcileOnce();
95
- } while (reconcileRequested && !disposed);
96
- }
97
- finally {
98
- reconciliation = undefined;
99
- }
100
- })();
101
- return reconciliation;
102
- };
103
76
  /**
104
- * Coalesce credential updates.
105
- *
106
- * `runtime.usesCredential(ref)` reflects the last completed pass, so an event
107
- * arriving while the first reconcile is still running would be filtered out
108
- * and the remount lost. Pending refs are therefore kept until the next pass
109
- * observes them, and bursts are collapsed into one reconcile.
77
+ * How long the change pipeline holds the queue on one stage. A mount that
78
+ * stops responding — a stdio server that never finishes its handshake, a
79
+ * browser authorization nobody completed — delays its own surface only; the
80
+ * stage keeps running while the pipeline moves on to the next change.
110
81
  */
111
- const pendingCredentialRefs = new Set();
112
- let credentialFlush;
113
- const flushCredentialUpdates = () => {
114
- credentialFlush = undefined;
115
- const refs = [...pendingCredentialRefs];
116
- pendingCredentialRefs.clear();
117
- // Re-read the catalog first: an unknown ref means the snapshot predates
118
- // this change, so reconcile anyway to refresh the known reference set.
119
- void reconcileMounts().catch(() => { });
120
- if (refs.length > 0) {
121
- ctx.logger?.info?.(`[dsh-agent-plugins-market] reconciling after credential update: ${refs.join(', ')}`);
82
+ const CHANGE_STAGE_DEADLINE_MS = 20_000;
83
+ /** Run one pipeline stage, logging its failure or its overrun instead of holding the pipeline. */
84
+ const runStage = async (stage, work) => {
85
+ const running = (async () => await work())();
86
+ void running.catch(error => {
87
+ ctx.logger?.warn(`[dsh-agent-plugins-market] ${stage} failed: ${error instanceof Error ? error.message : String(error)}`);
88
+ });
89
+ if (!(await settlesWithin(running, CHANGE_STAGE_DEADLINE_MS))) {
90
+ ctx.logger?.warn(`[dsh-agent-plugins-market] ${stage} is still running after ${CHANGE_STAGE_DEADLINE_MS}ms; the change pipeline continues without waiting for it`);
122
91
  }
123
92
  };
93
+ /**
94
+ * Catalog change pipeline: invalidate the derived skill and command
95
+ * surfaces, refresh the project mounts, then reconcile every runtime mount.
96
+ * `CatalogContext` schedules it in the background, so a stage that overruns
97
+ * costs freshness, never the mutation that asked for it.
98
+ */
124
99
  const onChanged = async () => {
125
100
  if (disposed)
126
101
  return;
127
102
  providerControl?.invalidate();
128
103
  userPanelControl?.invalidate();
129
- await projectCommands?.refresh();
130
- await suitePrompts?.refresh();
131
- await Promise.all([projectMcp?.refresh(), projectHooks?.refresh()]);
132
- const userCommandDiagnostics = await userCommands.reconcile().catch(() => []);
133
- for (const reason of userCommandDiagnostics) {
134
- ctx.logger?.warn(`[dsh-agent-plugins-market] user commands: ${reason}`);
135
- }
136
- await reconcileMounts();
104
+ await runStage('project commands', () => projectCommands?.refresh());
105
+ await runStage('suite instructions', () => suitePrompts?.refresh());
106
+ await runStage('project MCP and hooks', () => Promise.all([projectMcp?.refresh(), projectHooks?.refresh()]));
107
+ await runStage('user commands', () => reconcileUserCommands());
108
+ await runStage('runtime mounts', () => scheduler.request());
109
+ };
110
+ // Background source updates: off until the settings switch says otherwise.
111
+ const autoUpdate = new SourceAutoUpdater(ctx, () => catalog.refreshSource());
112
+ const settings = new MarketSettingsNamespace(ctx, dataRoot, hostLocale, {
113
+ setScanProjectLayouts: enabled => catalog.setScanProjectLayouts(enabled),
114
+ refreshMcpMounts: () => {
115
+ void Promise.all([scheduler.request(), projectMcp?.refresh()]).catch(() => { });
116
+ },
117
+ setAutoUpdateSources: enabled => autoUpdate.setEnabled(enabled)
118
+ });
119
+ // The credentials store powers the MCP re-authorize action (dropping a grant
120
+ // record forces the next mount through a fresh browser authorization). Every
121
+ // port below is read at call time: this plugin's apply may run before the
122
+ // credentials, tools and settings services provision, and a snapshot taken
123
+ // here would be permanently undefined even after the service is live.
124
+ // Read at call time: the tools service provisions after apply() returns.
125
+ let toolsRegistry = toolsServiceOf(ctx);
126
+ const ports = {
127
+ mcpToolSnapshot: () => inspectToolRegistry(toolsRegistry),
128
+ credentialsStore: {
129
+ deleteGrantRecord: async (serverName) => {
130
+ const store = ctx.get?.('credentials');
131
+ await deleteMcpAuthGrant(store, serverName);
132
+ }
133
+ },
134
+ // Re-authorize must rebuild the live bridge, not just drop the grant: the
135
+ // registry flags the mount and the following reconcile tears it down and
136
+ // remounts, so the server's 401 restarts the browser authorization.
137
+ mcpRemount: (suiteId, serverKey) => runtime.forceMcpRemount(suiteId, serverKey),
138
+ mcpRemountAll: () => runtime.forceMcpRemountAll(),
139
+ mcpServerOwner: serverName => runtime.mcpServerOwner(serverName),
140
+ lspStatusSource: runtime.lsp,
141
+ mcpBackend: () => settings.backend(),
142
+ setMcpBackend: backend => settings.setBackend(backend),
143
+ downloadRegion: () => settings.downloadRegion()
137
144
  };
138
- // The credential store powers the MCP re-authorize action (dropping a grant
139
- // record forces the next mount through a fresh browser authorization).
140
- // Resolved lazily at call time: this plugin's apply may run before the
141
- // credentials plugin provisions, and a snapshot taken here would be
142
- // permanently undefined even after the service is live.
143
- const catalog = new Catalog({ userRoot, dataRoot, onChanged, ...(config.git === undefined ? {} : { git: config.git }) });
145
+ const catalog = new Catalog({ userRoot, dataRoot, agentsRoot, onChanged, ports, ...(config.git === undefined ? {} : { git: config.git }) });
144
146
  await catalog.load();
145
147
  await catalog.mergeSources(config.sources ?? []);
146
148
  const resources = createPanelResources(catalog, panels);
147
- // Mirror oauth.ts's `credentialIdFor`: the record is stored under the folded
148
- // serverName, so the delete must fold the same way or it misses the record.
149
- const credentialIdFor = (serverName) => serverName
150
- .toLowerCase()
151
- .replace(/[^a-z0-9-]+/g, '-')
152
- .replace(/^-+|-+$/g, '') || 'server';
153
- catalog.setCredentialsStore({
154
- deleteGrantRecord: async (serverName) => {
155
- const store = ctx.get?.('credentials');
156
- if (store?.deleteRecord === undefined)
157
- throw new Error('credentials service is not mounted');
158
- await store.deleteRecord(`mcp-auth/${credentialIdFor(serverName)}`);
159
- }
160
- });
161
149
  runtime.setMcpOverridesProvider(async () => catalog.allMcpOverrides(await catalog.enabledUserSuites()));
162
- // Re-authorize must rebuild the live bridge, not just drop the grant: the
163
- // registry flags the mount and the following reconcile tears it down and
164
- // remounts, so the server's 401 restarts the browser authorization.
165
- catalog.setMcpRemountHook((suiteId, serverKey) => runtime.forceMcpRemount(suiteId, serverKey), serverName => runtime.mcpServerOwner(serverName));
166
- catalog.setLspStatusSource(runtime.lsp);
167
- runtime.lsp.setDirectProvider(async () => (await loadLspServers(dataRoot)).servers);
150
+ runtime.lsp.setDirectProvider(async () => (await loadLspServers(agentsRoot)).servers);
168
151
  runtime.lsp.setDisabledProvider(() => loadDisabledLspServers(dataRoot));
169
- // The MCP backend choice is a host settings namespace (the registration is
170
- // also what makes the plugin-config tab serve our card). The node half owns
171
- // it: the provider derives the mount backend from the scope, the writer
172
- // flips the switch, and the watcher remounts servers when it flips. The
173
- // same single registration also gates the experience-feedback tool —
174
- // `settings.register` throws on a duplicate namespace, so there is exactly
175
- // one inject block and one `register` call.
176
- //
177
- // The whole callback is failure-contained on purpose: a throw here is
178
- // INVISIBLE in the UI (the inject resolves asynchronously and cordis only
179
- // logs), and it silently removes the namespace from settings.describe —
180
- // which is exactly what hides the plugin-config card. Every failure mode
181
- // lands in the logger with a loud prefix instead.
182
- let feedbackDisposer;
183
- ctx.inject(['settings'], settingsCtx => {
184
- try {
185
- ctx.logger?.info?.('[dsh-agent-plugins-market] settings inject resolved — registering namespace');
186
- const settings = settingsCtx.settings;
187
- const scope = settings.register(MCP_SETTINGS_NAMESPACE, MarketSettingsSchema, {
188
- base: { mcpEnhanced: true, downloadRegion: 'auto', feedbackEnabled: true, scanProjectLayouts: true }
189
- });
190
- ctx.logger?.info?.('[dsh-agent-plugins-market] settings namespace registered — plugin-config card will serve');
191
- catalog.setMcpBackendProvider(async () => (scope.get().mcpEnhanced === false ? 'host' : 'builtin'));
192
- catalog.setMcpBackendWriter(async (backend) => {
193
- await scope.update({ mcpEnhanced: backend !== 'host' });
194
- });
195
- catalog.setDownloadRegionProvider(async () => narrowDownloadRegion(scope.get().downloadRegion));
196
- const syncProjectLayouts = () => {
197
- void catalog.setScanProjectLayouts(scope.get().scanProjectLayouts !== false).catch(error => {
198
- ctx.logger?.error?.(`[dsh-agent-plugins-market] project layout reconciliation failed: ${String(error)}`);
199
- });
200
- };
201
- syncProjectLayouts();
202
- settingsWatchers.push(scope.watch(syncProjectLayouts));
203
- // One-time migration from the earlier data-root settings.json choice.
204
- void readMcpBackend(dataRoot).then(backend => {
205
- if (backend === 'host')
206
- void scope.update({ mcpEnhanced: false }).catch(() => { });
207
- });
208
- let previousBackend = scope.get().mcpEnhanced !== false;
209
- settingsWatchers.push(scope.watch(() => {
210
- const backend = scope.get().mcpEnhanced !== false;
211
- if (backend === previousBackend)
212
- return;
213
- previousBackend = backend;
214
- void Promise.all([reconcileMounts(), projectMcp?.refresh()]).catch(() => { });
215
- }));
216
- // The experience-feedback model tool: gated by the namespace's
217
- // `feedbackEnabled` field (default on); the switch unregisters it. The
218
- // tool mount is doubly contained — its failure must never take the
219
- // settings namespace (and with it the config card) down with it.
220
- const syncFeedbackTool = () => {
221
- try {
222
- const state = scope.get();
223
- const wanted = state.feedbackEnabled !== false;
224
- if (wanted && feedbackDisposer === undefined) {
225
- feedbackDisposer = mountFeedbackTool(ctx, dataRoot, (key, params) => hostLocale.t(key, params)) ?? undefined;
226
- }
227
- else if (!wanted && feedbackDisposer !== undefined) {
228
- feedbackDisposer();
229
- feedbackDisposer = undefined;
230
- }
231
- }
232
- catch (error) {
233
- ctx.logger?.error?.(`[dsh-agent-plugins-market] feedback tool mount failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
234
- }
235
- };
236
- syncFeedbackTool();
237
- settingsWatchers.push(scope.watch(() => syncFeedbackTool()));
238
- }
239
- catch (error) {
240
- ctx.logger?.error?.(`[dsh-agent-plugins-market] settings namespace registration failed — the plugin-config card will be hidden this boot: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
241
- }
242
- });
243
152
  runtime.setMcpBackendProvider(() => catalog.mcpBackend());
244
- const eventHost = ctx;
245
- const disposeCredentialUpdates = eventHost.on?.('credentials/reference-updated', ref => {
246
- pendingCredentialRefs.add(ref);
247
- if (credentialFlush !== undefined)
248
- clearTimeout(credentialFlush);
249
- credentialFlush = setTimeout(flushCredentialUpdates, 150);
250
- credentialFlush.unref?.();
251
- }) ?? (() => { });
153
+ // The namespace must be registered after the catalog exists: the host
154
+ // resolves the inject callback synchronously when the settings service is
155
+ // already mounted, and the registration reads back the project-layout switch.
156
+ settings.mount();
252
157
  ctx.inject(['tools'], toolsCtx => {
253
- const registry = toolsCtx.tools;
254
- catalog.setMcpToolSnapshotProvider(() => inspectToolRegistry(registry));
158
+ toolsRegistry = toolsCtx.tools;
255
159
  // Foreign-namespace guard: a native host MCP client (or another plugin)
256
160
  // owning `mcp__<serverName>__` names makes the mount registry skip its
257
161
  // own server with a clear diagnostic instead of failing mid-registration.
258
- runtime.setMcpToolNamesProvider(() => inspectToolRegistry(registry).map(tool => tool.name));
162
+ runtime.setMcpToolNamesProvider(() => inspectToolRegistry(toolsRegistry).map(tool => tool.name));
259
163
  });
260
164
  void Promise.resolve()
261
165
  .then(async () => {
262
- const userCommandDiagnostics = await userCommands.reconcile().catch(() => []);
263
- for (const reason of userCommandDiagnostics) {
264
- ctx.logger?.warn(`[dsh-agent-plugins-market] user commands: ${reason}`);
265
- }
266
- await reconcileMounts();
166
+ await reconcileUserCommands();
167
+ await scheduler.request();
267
168
  })
268
169
  .catch(() => { });
170
+ // The shell seam resolves lazily: the service may land after this plugin, and
171
+ // a profile without one keeps dynamic-context placeholders literal.
172
+ const shellSeam = () => shellSeamOf(ctx);
269
173
  ctx.skills.registerProvider(control => {
270
174
  providerControl = control;
271
- return new SuiteSkillProvider(catalog);
175
+ return new SuiteSkillProvider(catalog, { dataRoot, shell: shellSeam });
272
176
  });
273
177
  // User panel skills ride a second provider so a panel
274
178
  // edit invalidates only its own catalog contribution.
275
179
  ctx.skills.registerProvider(control => {
276
180
  userPanelControl = control;
277
- return new UserPanelSkillProvider(panels.skills, key => hostLocale.t(key));
181
+ return new UserPanelSkillProvider(panels.skills);
278
182
  });
279
183
  ctx.inject(['tools', 'llm', 'subagents', 'agents'], hostCtx => {
280
184
  hostCtx.effect(() => mountAgentRoleTool(hostCtx, async (parent) => [
@@ -284,10 +188,10 @@ export async function apply(ctx, config = {}) {
284
188
  });
285
189
  ctx.inject(['agents'], hostCtx => {
286
190
  hostCtx.effect(() => {
287
- const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key));
191
+ const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key), dataRoot);
288
192
  const mcp = mountProjectMcp(hostCtx, catalog, dataRoot);
289
193
  const hooks = mountProjectHooks(hostCtx, catalog);
290
- const prompts = mountSuiteInstructions(hostCtx, catalog);
194
+ const prompts = mountSuiteInstructions(hostCtx, catalog, dataRoot);
291
195
  projectCommands = mounted;
292
196
  projectMcp = mcp;
293
197
  projectHooks = hooks;
@@ -310,15 +214,9 @@ export async function apply(ctx, config = {}) {
310
214
  });
311
215
  ctx.effect(() => () => {
312
216
  disposed = true;
313
- disposeCredentialUpdates();
314
- if (credentialFlush !== undefined)
315
- clearTimeout(credentialFlush);
316
- credentialFlush = undefined;
317
- pendingCredentialRefs.clear();
318
- for (const dispose of settingsWatchers.splice(0))
319
- dispose();
320
- feedbackDisposer?.();
321
- feedbackDisposer = undefined;
217
+ scheduler.dispose();
218
+ autoUpdate.dispose();
219
+ settings.dispose();
322
220
  userCommands.disposeAll();
323
221
  void runtime.dispose();
324
222
  }, 'dsh-agent-plugins-market: lifecycle');
@@ -15,6 +15,10 @@ export const MANIFEST_ALIASES = { kimi: ['kimi.plugin.json'] };
15
15
  /** Equivalent plugin-owned path variables; these never resolve as credentials. */
16
16
  export const PLUGIN_ROOT_VARIABLES = new Set(['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'ZCODE_PLUGIN_ROOT', 'QODER_PLUGIN_ROOT']);
17
17
  export const PLUGIN_DATA_VARIABLES = new Set(['PLUGIN_DATA', 'CLAUDE_PLUGIN_DATA', 'ZCODE_PLUGIN_DATA', 'QODER_PLUGIN_DATA']);
18
+ /** Equivalent session project-directory variables; only a calling session supplies the directory. */
19
+ export const PROJECT_DIR_VARIABLES = new Set(['CLAUDE_PROJECT_DIR', 'ZCODE_PROJECT_DIR', 'QODER_PROJECT_DIR']);
20
+ /** Claude Code names the directory holding the skill's own file; no dialect alias exists. */
21
+ export const SKILL_DIR_VARIABLE = 'CLAUDE_SKILL_DIR';
18
22
  export const PROJECT_LAYOUTS = [
19
23
  {
20
24
  dirName: '.claude',
@@ -23,7 +27,12 @@ export const PROJECT_LAYOUTS = [
23
27
  mcpFiles: ['.mcp.json'],
24
28
  hookFiles: ['.claude/settings.json', '.claude/settings.local.json']
25
29
  },
26
- { dirName: '.agents', label: 'agents', subdirs: ['skills', 'agents', 'commands'] },
30
+ {
31
+ dirName: '.agents',
32
+ label: 'agents',
33
+ subdirs: ['skills', 'agents', 'commands'],
34
+ hookFiles: ['.agents/hooks/hooks.json', '.agents/hooks.json']
35
+ },
27
36
  { dirName: '.codex', label: 'Codex', subdirs: ['skills'], mcpFiles: ['.codex/config.toml'], mcpFormat: 'codex' },
28
37
  { dirName: '.cursor', label: 'Cursor', subdirs: ['skills', 'agents', 'commands'], mcpFiles: ['.cursor/mcp.json'] },
29
38
  { dirName: '.kimi', label: 'Kimi', subdirs: ['skills'] },
@@ -7,11 +7,34 @@
7
7
  * Codex `.codex-plugin/plugin.json`). Discovery maps every layout onto this
8
8
  * internal shape; runtime injection consumes only the internal shape.
9
9
  */
10
- /** Archive URL extensions the archive acquisition path understands. */
11
- const ARCHIVE_URL_PATTERN = /\.(zip|tgz|tar\.gz|tar)$/i;
10
+ /**
11
+ * Archive URL suffixes with the payload kind each names, longest suffix first
12
+ * so `.tar.gz` wins over `.tar`. Source-kind inference, source-id derivation,
13
+ * and archive extraction all read this one table.
14
+ */
15
+ export const ARCHIVE_SUFFIXES = [
16
+ ['.tar.gz', 'targz'],
17
+ ['.tgz', 'targz'],
18
+ ['.zip', 'zip'],
19
+ ['.tar', 'tar']
20
+ ];
21
+ /** The archive suffix `url` ends with, or undefined when it names no known payload. */
22
+ function matchArchiveSuffix(url) {
23
+ const clean = url.trim().toLowerCase();
24
+ return ARCHIVE_SUFFIXES.find(([suffix]) => clean.endsWith(suffix));
25
+ }
26
+ /** Classify an archive URL by extension; undefined when unsupported. */
27
+ export function archiveFormatOf(url) {
28
+ return matchArchiveSuffix(url)?.[1];
29
+ }
12
30
  /** Whether a URL points at a downloadable archive. */
13
31
  export function isArchiveUrl(url) {
14
- return ARCHIVE_URL_PATTERN.test(url.trim());
32
+ return matchArchiveSuffix(url) !== undefined;
33
+ }
34
+ /** Drop a trailing archive suffix (`plugin-0.1.zip` → `plugin-0.1`); other values pass through. */
35
+ export function stripArchiveSuffix(value) {
36
+ const suffix = matchArchiveSuffix(value)?.[0];
37
+ return suffix === undefined ? value : value.slice(0, -suffix.length);
15
38
  }
16
39
  /**
17
40
  * Effective acquisition kind of a source: the explicit `kind` wins, the