@ganju/utils 0.0.10 → 0.0.12

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/src/index.ts CHANGED
@@ -117,7 +117,11 @@ import type { OAuthProviderConfig } from './oauthProviders';
117
117
  import { resolveAttachment } from './attachment';
118
118
  import { isExposedResource } from './exposedResource';
119
119
  import type { ExposableResource } from './exposedResource';
120
- import { isSyncableResource, resourceSyncProvider } from './syncableResource';
120
+ import {
121
+ isSyncableResource,
122
+ isResourceSyncInProgress,
123
+ resourceSyncProvider
124
+ } from './syncableResource';
121
125
  import type {
122
126
  SyncableResource,
123
127
  ResourceSyncProvider
@@ -169,6 +173,7 @@ import type {
169
173
  CatalogGroup,
170
174
  CatalogTool,
171
175
  CatalogToolDescriptor,
176
+ ToolAnnotations,
172
177
  ToolGroupKey,
173
178
  ToolKey
174
179
  } from './toolCatalog';
@@ -220,6 +225,35 @@ import type {
220
225
  ChannelBufferEnvelope,
221
226
  ChannelBufferFlush
222
227
  } from './channelDebounce';
228
+ import {
229
+ isConfirmationReply,
230
+ confirmationButtonLabels,
231
+ confirmationButtonId,
232
+ parseConfirmationButton,
233
+ languageFromPhoneNumber,
234
+ formatConfirmationSummary,
235
+ annotationsForEffect,
236
+ TOOL_EFFECTS,
237
+ isSensitiveTool,
238
+ stableJson
239
+ } from './toolConfirmation';
240
+ import type {
241
+ PendingToolConfirmation,
242
+ PendingToolConfirmations,
243
+ ToolEffect
244
+ } from './toolConfirmation';
245
+ import {
246
+ lintTools,
247
+ TOOL_LINT_RULES,
248
+ TOOL_LINT_MAX_TOOLS,
249
+ TOOL_LINT_MIN_DESCRIPTION_LENGTH
250
+ } from './toolLint';
251
+ import type {
252
+ LintableTool,
253
+ ToolLintFinding,
254
+ ToolLintOptions,
255
+ ToolLintRule
256
+ } from './toolLint';
223
257
  import {
224
258
  isResourceSourceEnabled,
225
259
  isDownloadableSource,
@@ -301,6 +335,7 @@ export const utils = {
301
335
  resolveAttachment,
302
336
  isExposedResource,
303
337
  isSyncableResource,
338
+ isResourceSyncInProgress,
304
339
  resourceSyncProvider,
305
340
  describeVendorError,
306
341
  withDeadline,
@@ -358,6 +393,20 @@ export const utils = {
358
393
  channelBufferKey,
359
394
  batchCommand,
360
395
  joinBufferedMessages,
396
+ isConfirmationReply,
397
+ confirmationButtonLabels,
398
+ confirmationButtonId,
399
+ parseConfirmationButton,
400
+ languageFromPhoneNumber,
401
+ formatConfirmationSummary,
402
+ annotationsForEffect,
403
+ TOOL_EFFECTS,
404
+ isSensitiveTool,
405
+ stableJson,
406
+ lintTools,
407
+ TOOL_LINT_RULES,
408
+ TOOL_LINT_MAX_TOOLS,
409
+ TOOL_LINT_MIN_DESCRIPTION_LENGTH,
361
410
  isResourceSourceEnabled,
362
411
  isDownloadableSource,
363
412
  safeHostname,
@@ -386,6 +435,14 @@ export type {
386
435
  CatalogGroup,
387
436
  CatalogTool,
388
437
  CatalogToolDescriptor,
438
+ ToolAnnotations,
439
+ PendingToolConfirmation,
440
+ PendingToolConfirmations,
441
+ ToolEffect,
442
+ LintableTool,
443
+ ToolLintFinding,
444
+ ToolLintOptions,
445
+ ToolLintRule,
389
446
  ToolGroupKey,
390
447
  ToolKey,
391
448
  CalendarConfigField,
package/src/schema.ts CHANGED
@@ -5,6 +5,7 @@ import { isAllowedLlmBaseUrl, requiresLlmBaseUrl } from './llmBaseUrl';
5
5
  import { isReservedSlug, isValidSlugFormat } from './slug';
6
6
  import { isReservedToolName } from './reservedToolName';
7
7
  import { slugifyTitle } from './slugifyTitle';
8
+ import { TOOL_EFFECTS } from './toolConfirmation';
8
9
 
9
10
  // A prompt title becomes a slash command; it must not collide with a command
10
11
  // the channel runner handles itself (e.g. `/link`).
@@ -43,11 +44,20 @@ const ORGANIZATION_CREATE_VIEW = z.object({
43
44
  projectDescription: z.string().max(500)
44
45
  });
45
46
 
46
- const ORGANIZATION_UPDATE = z.object({
47
- id: z.uuid(),
48
- userId: z.uuid(),
49
- name: z.string().min(3).max(100)
50
- });
47
+ // Each setting is saved on its own — the name from its form, the switches the
48
+ // moment they're flipped — so every field is optional, but not all at once.
49
+ const ORGANIZATION_UPDATE = z
50
+ .object({
51
+ id: z.uuid(),
52
+ userId: z.uuid(),
53
+ name: z.string().min(3).max(100).optional(),
54
+ requireToolConfirmation: z.boolean().optional()
55
+ })
56
+ .refine(
57
+ value =>
58
+ value.name !== undefined || value.requireToolConfirmation !== undefined,
59
+ { message: 'Nothing to update' }
60
+ );
51
61
 
52
62
  const ORGANIZATION_GET = z.object({
53
63
  id: z.uuid(),
@@ -851,6 +861,12 @@ const HTTP_ENDPOINT_CONFIG = z
851
861
  contentType: constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_AUTO,
852
862
  maxBytes: constants.HTTP_ENDPOINT_DEFAULT_MAX_BYTES
853
863
  }),
864
+ // What the endpoint does, as its owner says (`read`, `write` or
865
+ // `sensitive`). Absent means the method decides: GET and HEAD read,
866
+ // anything else is sensitive — right for most, but a POST that only
867
+ // searches needs its owner to say so, or a confirming channel asks
868
+ // before every search.
869
+ effect: z.enum(TOOL_EFFECTS).optional(),
854
870
  // Auth — credentials referenced by id, never inlined.
855
871
  auth: HTTP_ENDPOINT_AUTH.default({
856
872
  kind: constants.HTTP_ENDPOINT_AUTH_KIND_NONE
@@ -1056,6 +1072,19 @@ const CUSTOM_CODE_CONFIG_WRITE = CUSTOM_CODE_CONFIG.superRefine((cfg, ctx) => {
1056
1072
  // that version registering, and the owner would see tools quietly disappear with
1057
1073
  // only a log line to explain it. Reservation belongs to the manifest below,
1058
1074
  // which is the write path.
1075
+ // MCP tool annotations as a tool's author declares them — every hint optional,
1076
+ // with the spec's own defaults for what is left out. The channel runner reads
1077
+ // them to decide what to confirm, so a tool that doesn't say it is read-only
1078
+ // is treated as one that may change things.
1079
+ const TOOL_ANNOTATIONS = z
1080
+ .object({
1081
+ readOnlyHint: z.boolean().optional(),
1082
+ destructiveHint: z.boolean().optional(),
1083
+ idempotentHint: z.boolean().optional(),
1084
+ openWorldHint: z.boolean().optional()
1085
+ })
1086
+ .strict();
1087
+
1059
1088
  const CUSTOM_CODE_TOOL = z.object({
1060
1089
  name: z
1061
1090
  .string()
@@ -1068,7 +1097,8 @@ const CUSTOM_CODE_TOOL = z.object({
1068
1097
  title: z.string().min(1).max(200).optional(),
1069
1098
  description: z.string().max(2000).optional(),
1070
1099
  inputSchema: SCHEMA_DEFINITION.default({ type: 'object', properties: {} }),
1071
- outputSchema: SCHEMA_DEFINITION.optional()
1100
+ outputSchema: SCHEMA_DEFINITION.optional(),
1101
+ annotations: TOOL_ANNOTATIONS.optional()
1072
1102
  });
1073
1103
 
1074
1104
  // The manifest a `ganju deploy` (or the dashboard) uploads alongside a bundle.
@@ -1648,6 +1678,7 @@ export const Schema = {
1648
1678
  CUSTOM_CODE_CONFIG,
1649
1679
  CUSTOM_CODE_CONFIG_WRITE,
1650
1680
  CUSTOM_CODE_TOOL,
1681
+ TOOL_ANNOTATIONS,
1651
1682
  CUSTOM_CODE_MANIFEST,
1652
1683
  ARTIFACT_CUSTOM_CODE_CREATE_VERSION,
1653
1684
  ARTIFACT_CUSTOM_CODE_UPLOAD_BUNDLE,
@@ -1707,6 +1738,10 @@ export interface McpProxyDiscoveredTool {
1707
1738
  title?: string;
1708
1739
  description?: string;
1709
1740
  inputSchema: unknown;
1741
+ // The remote's own annotations, passed through at registration. Absent on
1742
+ // discoveries made before they were recorded — the tool then counts as one
1743
+ // that may change things until the server is discovered again.
1744
+ annotations?: z.infer<typeof TOOL_ANNOTATIONS>;
1710
1745
  }
1711
1746
 
1712
1747
  // One remote resource discovered from a proxied MCP server. Identified by uri;
@@ -46,3 +46,23 @@ export const resourceSyncProvider = (
46
46
 
47
47
  export const isSyncableResource = (resource: SyncableResource): boolean =>
48
48
  resourceSyncProvider(resource) !== null;
49
+
50
+ // Whether a sync root is still genuinely syncing: PENDING, and started within
51
+ // RESOURCE_SYNC_STALE_MS. Past that, a PENDING root is stuck — a job was lost
52
+ // or gave up — and may be synced again, which re-queues whatever it left
53
+ // unfinished. A root that never synced counts from its creation, so a first
54
+ // import that stalled recovers too.
55
+ export const isResourceSyncInProgress = (
56
+ resource: {
57
+ status: string;
58
+ syncStartedAt?: Date | string | null;
59
+ createdAt: Date | string;
60
+ },
61
+ now: number = Date.now()
62
+ ): boolean => {
63
+ if (resource.status !== constants.STATUS_PENDING) return false;
64
+ const startedAt = new Date(
65
+ resource.syncStartedAt ?? resource.createdAt
66
+ ).getTime();
67
+ return now - startedAt < constants.RESOURCE_SYNC_STALE_MS;
68
+ };