imprint-mcp 0.4.0 → 0.4.2

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.
@@ -27,6 +27,7 @@ import {
27
27
  import * as p from '@clack/prompts';
28
28
  import YAML from 'yaml';
29
29
  import { imprintHomeDir, localSiteDir } from './paths.ts';
30
+ import { type BackendsCacheStatus, loadBackendsCacheStatus } from './probe-backends.ts';
30
31
  import {
31
32
  type WorkflowState,
32
33
  loadTeachState,
@@ -38,7 +39,7 @@ import {
38
39
 
39
40
  type McpClient = 'claude-code' | 'codex' | 'claude-desktop' | 'openclaw' | 'hermes';
40
41
  type LocalDeleteMode = 'none' | 'tool' | 'site';
41
- type IssueKind = 'missing-session' | 'stale-registration';
42
+ type IssueKind = 'missing-session' | 'stale-registration' | 'stale-backends' | 'invalid-backends';
42
43
 
43
44
  const CLIENTS: McpClient[] = ['claude-code', 'codex', 'claude-desktop', 'openclaw', 'hermes'];
44
45
  const DISABLED_STORE_VERSION = 1;
@@ -81,6 +82,15 @@ interface LocalToolStatus {
81
82
  hasPlaybook: boolean;
82
83
  hasBackends: boolean;
83
84
  hasCron: boolean;
85
+ backendCache: PublicBackendsCacheStatus;
86
+ }
87
+
88
+ interface PublicBackendsCacheStatus {
89
+ status: BackendsCacheStatus['status'];
90
+ path: string | null;
91
+ preferredOrder?: string[];
92
+ reason?: string;
93
+ remediation?: string;
84
94
  }
85
95
 
86
96
  interface LocalWorkflowStatus {
@@ -475,7 +485,13 @@ function cmdStatus(argv: string[]): number {
475
485
  const status = scanMcpStatus({ site });
476
486
  if (flags.json === true) console.log(JSON.stringify(status, null, 2));
477
487
  else console.log(formatMcpStatus(status));
478
- return status.issues.some((i) => i.kind === 'stale-registration' || i.kind === 'missing-session')
488
+ return status.issues.some(
489
+ (i) =>
490
+ i.kind === 'stale-registration' ||
491
+ i.kind === 'missing-session' ||
492
+ i.kind === 'stale-backends' ||
493
+ i.kind === 'invalid-backends',
494
+ )
479
495
  ? 1
480
496
  : 0;
481
497
  }
@@ -691,6 +707,11 @@ function issueFixHint(issue: McpIssue): string | null {
691
707
  return `choose "Fix an issue" or run: imprint mcp delete ${issue.name ?? `imprint-${issue.site}`} --client ${issue.client ?? 'all'} --yes`;
692
708
  case 'missing-session':
693
709
  return `choose "Fix an issue" or run: imprint mcp prune-state --site ${issue.site} --missing-session --yes`;
710
+ case 'stale-backends':
711
+ case 'invalid-backends':
712
+ return issue.path
713
+ ? `run: imprint probe-backends ${issue.site}${issue.workflow ? ` --tool ${issue.workflow}` : ''}`
714
+ : `run: imprint probe-backends ${issue.site}${issue.workflow ? ` --tool ${issue.workflow}` : ''}`;
694
715
  }
695
716
  return null;
696
717
  }
@@ -712,26 +733,32 @@ function scanLocalSites(ctx: MaintenanceContext): LocalSiteStatus[] {
712
733
  if (entry === 'node_modules' || entry.startsWith('.')) continue;
713
734
  const dir = pathJoin(ctx.imprintHome, entry);
714
735
  if (!safeIsDir(dir)) continue;
715
- sites.push(scanLocalSite(entry, dir));
736
+ sites.push(scanLocalSite(entry, dir, ctx.imprintHome));
716
737
  }
717
738
  return sites;
718
739
  }
719
740
 
720
- function scanLocalSite(site: string, dir: string): LocalSiteStatus {
741
+ function scanLocalSite(site: string, dir: string, imprintHome: string): LocalSiteStatus {
721
742
  const tools: LocalToolStatus[] = [];
722
743
  for (const entry of readdirSync(dir).sort()) {
723
744
  if (entry === 'sessions' || entry === '_shared' || entry.startsWith('.')) continue;
724
745
  const toolDir = pathJoin(dir, entry);
725
746
  if (!safeIsDir(toolDir)) continue;
747
+ const toolName = workflowJsonToolName(toolDir) ?? entry;
748
+ const cacheStatus = loadBackendsCacheStatus(site, imprintHome, toolDir, {
749
+ warn: false,
750
+ toolName,
751
+ });
726
752
  tools.push({
727
753
  site,
728
- toolName: entry,
754
+ toolName,
729
755
  dir: toolDir,
730
756
  complete: existsSync(pathJoin(toolDir, 'index.ts')),
731
757
  hasWorkflow: existsSync(pathJoin(toolDir, 'workflow.json')),
732
758
  hasPlaybook: existsSync(pathJoin(toolDir, 'playbook.yaml')),
733
759
  hasBackends: existsSync(pathJoin(toolDir, 'backends.json')),
734
760
  hasCron: existsSync(pathJoin(toolDir, 'cron.json')),
761
+ backendCache: publicBackendsCacheStatus(cacheStatus),
735
762
  });
736
763
  }
737
764
 
@@ -746,6 +773,29 @@ function scanLocalSite(site: string, dir: string): LocalSiteStatus {
746
773
  return { site, dir, tools, workflows };
747
774
  }
748
775
 
776
+ function publicBackendsCacheStatus(status: BackendsCacheStatus): PublicBackendsCacheStatus {
777
+ if (status.status === 'ok') {
778
+ return {
779
+ status: status.status,
780
+ path: status.path,
781
+ preferredOrder: status.cache.preferredOrder,
782
+ };
783
+ }
784
+ if (status.status === 'missing') {
785
+ return {
786
+ status: status.status,
787
+ path: status.path,
788
+ remediation: status.remediation,
789
+ };
790
+ }
791
+ return {
792
+ status: status.status,
793
+ path: status.path,
794
+ reason: status.reason,
795
+ remediation: status.remediation,
796
+ };
797
+ }
798
+
749
799
  function workflowStatus(
750
800
  site: string,
751
801
  name: string,
@@ -795,6 +845,21 @@ function collectIssues(opts: {
795
845
  const sitesByName = new Map(opts.sites.map((s) => [s.site, s]));
796
846
 
797
847
  for (const site of opts.sites) {
848
+ for (const tool of site.tools) {
849
+ if (tool.backendCache.status === 'stale' || tool.backendCache.status === 'invalid') {
850
+ issues.push({
851
+ kind: tool.backendCache.status === 'stale' ? 'stale-backends' : 'invalid-backends',
852
+ site: site.site,
853
+ workflow: tool.toolName,
854
+ path: tool.backendCache.path ?? undefined,
855
+ message:
856
+ tool.backendCache.status === 'stale'
857
+ ? `${site.site}/${tool.toolName} has a stale backends.json; runtime will fall back to the default ladder until reprobed`
858
+ : `${site.site}/${tool.toolName} has an invalid backends.json; runtime will fall back to the default ladder until reprobed`,
859
+ });
860
+ }
861
+ }
862
+
798
863
  for (const wf of site.workflows) {
799
864
  if (wf.missingSession) {
800
865
  issues.push({
@@ -1091,7 +1156,7 @@ function pruneTeachState(
1091
1156
  for (const site of sites) {
1092
1157
  const statePath = teachStatePath(site);
1093
1158
  if (!existsSync(statePath)) continue;
1094
- const status = scanLocalSite(site, localSiteDir(site));
1159
+ const status = scanLocalSite(site, localSiteDir(site), ctx.imprintHome);
1095
1160
  const remove = new Set(
1096
1161
  status.workflows
1097
1162
  .filter(
@@ -20,7 +20,7 @@ import { resolveLadder, runWithLadder } from './backend-ladder.ts';
20
20
  import type { CdpBrowserFetch } from './cdp-browser-fetch.ts';
21
21
  import { createLog } from './log.ts';
22
22
  import { imprintHomeDir } from './paths.ts';
23
- import { loadBackendsCache } from './probe-backends.ts';
23
+ import { loadBackendsCacheStatus, persistRuntimeBackendsCache } from './probe-backends.ts';
24
24
  import { checkSiteCredentialsReady } from './runtime.ts';
25
25
  import { availableSitesHint } from './sites.ts';
26
26
  import type { StealthFetch } from './stealth-fetch.ts';
@@ -173,7 +173,7 @@ function buildServer(
173
173
 
174
174
  try {
175
175
  const ladder = resolveLadder('auto', tool.preferredOrder);
176
- const { result, usedBackend } = await runWithLadder(
176
+ const { result, usedBackend, attempts } = await runWithLadder(
177
177
  ladder,
178
178
  tool,
179
179
  args,
@@ -209,6 +209,24 @@ function buildServer(
209
209
  content: [{ type: 'text', text: `${text}\n(backend: ${usedBackend})` }],
210
210
  };
211
211
  }
212
+ try {
213
+ const cache = persistRuntimeBackendsCache({
214
+ tool,
215
+ assetRoot,
216
+ usedBackend,
217
+ attempts,
218
+ });
219
+ if (cache) {
220
+ tool.preferredOrder = cache.preferredOrder;
221
+ log(
222
+ ` learned backend order for ${tool.workflow.toolName}: ${cache.preferredOrder.join(' → ')}`,
223
+ );
224
+ }
225
+ } catch (err) {
226
+ log(
227
+ ` warning: could not persist backend order for ${tool.workflow.toolName}: ${err instanceof Error ? err.message : String(err)}`,
228
+ );
229
+ }
212
230
  const text =
213
231
  typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
214
232
  return { content: [{ type: 'text', text: `${text}\n\n(backend: ${usedBackend})` }] };
@@ -250,12 +268,19 @@ export async function runMcpServer(opts: RunMcpServerOptions): Promise<void> {
250
268
  const discovered = await discoverTools(assetRoot, opts.site, '[imprint mcp]');
251
269
  const tools: ResolvedTool[] = discovered.map((t) => {
252
270
  const playbookPath = pathResolve(t.dir, 'playbook.yaml');
253
- const cache = loadBackendsCache(t.site, assetRoot, t.dir);
271
+ const cacheStatus = loadBackendsCacheStatus(t.site, assetRoot, t.dir, {
272
+ toolName: t.workflow.toolName,
273
+ });
274
+ if (cacheStatus.status === 'stale' || cacheStatus.status === 'invalid') {
275
+ log(
276
+ ` ${t.workflow.toolName}: ${cacheStatus.status} backends.json (${cacheStatus.reason}); run \`${cacheStatus.remediation}\``,
277
+ );
278
+ }
254
279
  return {
255
280
  ...t,
256
281
  inputSchema: buildJsonSchema(t.workflow.parameters),
257
282
  playbookPath: existsSync(playbookPath) ? playbookPath : undefined,
258
- preferredOrder: cache?.preferredOrder,
283
+ preferredOrder: cacheStatus.status === 'ok' ? cacheStatus.cache.preferredOrder : undefined,
259
284
  };
260
285
  });
261
286
  if (tools.length === 0) {