diff --git a/lib/src/generated/fai/v1/hub.pb.dart b/lib/src/generated/fai/v1/hub.pb.dart index 3341919..5602d9b 100644 --- a/lib/src/generated/fai/v1/hub.pb.dart +++ b/lib/src/generated/fai/v1/hub.pb.dart @@ -2055,6 +2055,151 @@ class FetchModuleDocsResponse extends $pb.GeneratedMessage { void clearSourceUrl() => $_clearField(3); } +class GetInstalledModuleDocsRequest extends $pb.GeneratedMessage { + factory GetInstalledModuleDocsRequest({ + $core.String? name, + $core.String? locale, + }) { + final $result = create(); + if (name != null) { + $result.name = name; + } + if (locale != null) { + $result.locale = locale; + } + return $result; + } + GetInstalledModuleDocsRequest._() : super(); + factory GetInstalledModuleDocsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GetInstalledModuleDocsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetInstalledModuleDocsRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'name') + ..aOS(2, _omitFieldNames ? '' : 'locale') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetInstalledModuleDocsRequest clone() => GetInstalledModuleDocsRequest()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetInstalledModuleDocsRequest copyWith(void Function(GetInstalledModuleDocsRequest) updates) => super.copyWith((message) => updates(message as GetInstalledModuleDocsRequest)) as GetInstalledModuleDocsRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetInstalledModuleDocsRequest create() => GetInstalledModuleDocsRequest._(); + GetInstalledModuleDocsRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GetInstalledModuleDocsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GetInstalledModuleDocsRequest? _defaultInstance; + + /// Capability id (e.g. `text.extract`) or module-manifest name + /// (e.g. `text-extract`). The hub maps capability → module via + /// the registry; falls back to treating the name as a + /// directory name. + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => $_clearField(1); + + /// Preferred locale code (e.g. "de", "en"). When set and not + /// "en", `MODULE..md` is tried first; otherwise the + /// generic `MODULE.md` is used. Empty string = no preference. + @$pb.TagNumber(2) + $core.String get locale => $_getSZ(1); + @$pb.TagNumber(2) + set locale($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasLocale() => $_has(1); + @$pb.TagNumber(2) + void clearLocale() => $_clearField(2); +} + +class GetInstalledModuleDocsResponse extends $pb.GeneratedMessage { + factory GetInstalledModuleDocsResponse({ + $core.String? text, + $core.String? sourcePath, + $core.bool? notInstalled, + }) { + final $result = create(); + if (text != null) { + $result.text = text; + } + if (sourcePath != null) { + $result.sourcePath = sourcePath; + } + if (notInstalled != null) { + $result.notInstalled = notInstalled; + } + return $result; + } + GetInstalledModuleDocsResponse._() : super(); + factory GetInstalledModuleDocsResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GetInstalledModuleDocsResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetInstalledModuleDocsResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'text') + ..aOS(2, _omitFieldNames ? '' : 'sourcePath') + ..aOB(3, _omitFieldNames ? '' : 'notInstalled') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetInstalledModuleDocsResponse clone() => GetInstalledModuleDocsResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GetInstalledModuleDocsResponse copyWith(void Function(GetInstalledModuleDocsResponse) updates) => super.copyWith((message) => updates(message as GetInstalledModuleDocsResponse)) as GetInstalledModuleDocsResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetInstalledModuleDocsResponse create() => GetInstalledModuleDocsResponse._(); + GetInstalledModuleDocsResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GetInstalledModuleDocsResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GetInstalledModuleDocsResponse? _defaultInstance; + + /// Markdown body. Empty when the module is installed but ships + /// no inline docs — Studio uses this to hide its docs button. + @$pb.TagNumber(1) + $core.String get text => $_getSZ(0); + @$pb.TagNumber(1) + set text($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasText() => $_has(0); + @$pb.TagNumber(1) + void clearText() => $_clearField(1); + + /// Absolute path on the hub host that the body was read from, + /// for "loaded from /path/MODULE.md" hints in Studio. Empty + /// when text is empty. + @$pb.TagNumber(2) + $core.String get sourcePath => $_getSZ(1); + @$pb.TagNumber(2) + set sourcePath($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasSourcePath() => $_has(1); + @$pb.TagNumber(2) + void clearSourcePath() => $_clearField(2); + + /// True when the module wasn't installed at all. Distinct from + /// "installed but no inline docs" so Studio can show different + /// affordances ("install first" vs "no inline docs available"). + @$pb.TagNumber(3) + $core.bool get notInstalled => $_getBF(2); + @$pb.TagNumber(3) + set notInstalled($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasNotInstalled() => $_has(2); + @$pb.TagNumber(3) + void clearNotInstalled() => $_clearField(3); +} + class ListApprovalsRequest extends $pb.GeneratedMessage { factory ListApprovalsRequest({ $core.Iterable<$core.String>? statuses, diff --git a/lib/src/generated/fai/v1/hub.pbgrpc.dart b/lib/src/generated/fai/v1/hub.pbgrpc.dart index b643be5..e225ef6 100644 --- a/lib/src/generated/fai/v1/hub.pbgrpc.dart +++ b/lib/src/generated/fai/v1/hub.pbgrpc.dart @@ -175,6 +175,10 @@ class HubAdminClient extends $grpc.Client { '/fai.v1.HubAdmin/FetchModuleDocs', ($0.FetchModuleDocsRequest value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.FetchModuleDocsResponse.fromBuffer(value)); + static final _$getInstalledModuleDocs = $grpc.ClientMethod<$0.GetInstalledModuleDocsRequest, $0.GetInstalledModuleDocsResponse>( + '/fai.v1.HubAdmin/GetInstalledModuleDocs', + ($0.GetInstalledModuleDocsRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.GetInstalledModuleDocsResponse.fromBuffer(value)); static final _$listApprovals = $grpc.ClientMethod<$0.ListApprovalsRequest, $0.ApprovalList>( '/fai.v1.HubAdmin/ListApprovals', ($0.ListApprovalsRequest value) => value.writeToBuffer(), @@ -356,6 +360,16 @@ class HubAdminClient extends $grpc.Client { return $createUnaryCall(_$fetchModuleDocs, request, options: options); } + /// Read the inline-docs (MODULE.md / MODULE..md) of an + /// installed module from disk. No network fetch, no provider- + /// specific URL probing — the docs ship in the .fai bundle and + /// live next to module.wasm on disk after install. Returns an + /// empty body when the module is installed but ships no inline + /// docs; Studio uses that to hide the docs button entirely. + $grpc.ResponseFuture<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs($0.GetInstalledModuleDocsRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$getInstalledModuleDocs, request, options: options); + } + /// List system.approval@^0 entries, optionally filtered by status. $grpc.ResponseFuture<$0.ApprovalList> listApprovals($0.ListApprovalsRequest request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$listApprovals, request, options: options); @@ -639,6 +653,13 @@ abstract class HubAdminServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $0.FetchModuleDocsRequest.fromBuffer(value), ($0.FetchModuleDocsResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.GetInstalledModuleDocsRequest, $0.GetInstalledModuleDocsResponse>( + 'GetInstalledModuleDocs', + getInstalledModuleDocs_Pre, + false, + false, + ($core.List<$core.int> value) => $0.GetInstalledModuleDocsRequest.fromBuffer(value), + ($0.GetInstalledModuleDocsResponse value) => value.writeToBuffer())); $addMethod($grpc.ServiceMethod<$0.ListApprovalsRequest, $0.ApprovalList>( 'ListApprovals', listApprovals_Pre, @@ -875,6 +896,10 @@ abstract class HubAdminServiceBase extends $grpc.Service { return fetchModuleDocs($call, await $request); } + $async.Future<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs_Pre($grpc.ServiceCall $call, $async.Future<$0.GetInstalledModuleDocsRequest> $request) async { + return getInstalledModuleDocs($call, await $request); + } + $async.Future<$0.ApprovalList> listApprovals_Pre($grpc.ServiceCall $call, $async.Future<$0.ListApprovalsRequest> $request) async { return listApprovals($call, await $request); } @@ -992,6 +1017,7 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.InstallModuleResponse> installModule($grpc.ServiceCall call, $0.InstallModuleRequest request); $async.Future<$0.UninstallModuleResponse> uninstallModule($grpc.ServiceCall call, $0.UninstallModuleRequest request); $async.Future<$0.FetchModuleDocsResponse> fetchModuleDocs($grpc.ServiceCall call, $0.FetchModuleDocsRequest request); + $async.Future<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs($grpc.ServiceCall call, $0.GetInstalledModuleDocsRequest request); $async.Future<$0.ApprovalList> listApprovals($grpc.ServiceCall call, $0.ListApprovalsRequest request); $async.Future<$0.DecideApprovalResponse> decideApproval($grpc.ServiceCall call, $0.DecideApprovalRequest request); $async.Future<$0.VerifyEventChainResponse> verifyEventChain($grpc.ServiceCall call, $1.Empty request); diff --git a/lib/src/generated/fai/v1/hub.pbjson.dart b/lib/src/generated/fai/v1/hub.pbjson.dart index 48b8631..04132a5 100644 --- a/lib/src/generated/fai/v1/hub.pbjson.dart +++ b/lib/src/generated/fai/v1/hub.pbjson.dart @@ -532,6 +532,36 @@ final $typed_data.Uint8List fetchModuleDocsResponseDescriptor = $convert.base64D 'ChdGZXRjaE1vZHVsZURvY3NSZXNwb25zZRIdCgplcnJvcl9raW5kGAEgASgJUgllcnJvcktpbm' 'QSEgoEdGV4dBgCIAEoCVIEdGV4dBIdCgpzb3VyY2VfdXJsGAMgASgJUglzb3VyY2VVcmw='); +@$core.Deprecated('Use getInstalledModuleDocsRequestDescriptor instead') +const GetInstalledModuleDocsRequest$json = { + '1': 'GetInstalledModuleDocsRequest', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + {'1': 'locale', '3': 2, '4': 1, '5': 9, '10': 'locale'}, + ], +}; + +/// Descriptor for `GetInstalledModuleDocsRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getInstalledModuleDocsRequestDescriptor = $convert.base64Decode( + 'Ch1HZXRJbnN0YWxsZWRNb2R1bGVEb2NzUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lEhYKBm' + 'xvY2FsZRgCIAEoCVIGbG9jYWxl'); + +@$core.Deprecated('Use getInstalledModuleDocsResponseDescriptor instead') +const GetInstalledModuleDocsResponse$json = { + '1': 'GetInstalledModuleDocsResponse', + '2': [ + {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, + {'1': 'source_path', '3': 2, '4': 1, '5': 9, '10': 'sourcePath'}, + {'1': 'not_installed', '3': 3, '4': 1, '5': 8, '10': 'notInstalled'}, + ], +}; + +/// Descriptor for `GetInstalledModuleDocsResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getInstalledModuleDocsResponseDescriptor = $convert.base64Decode( + 'Ch5HZXRJbnN0YWxsZWRNb2R1bGVEb2NzUmVzcG9uc2USEgoEdGV4dBgBIAEoCVIEdGV4dBIfCg' + 'tzb3VyY2VfcGF0aBgCIAEoCVIKc291cmNlUGF0aBIjCg1ub3RfaW5zdGFsbGVkGAMgASgIUgxu' + 'b3RJbnN0YWxsZWQ='); + @$core.Deprecated('Use listApprovalsRequestDescriptor instead') const ListApprovalsRequest$json = { '1': 'ListApprovalsRequest', diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 82d84a7..9da5025 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -59,6 +59,7 @@ typedef ClearEventLogResponse = pb.ClearEventLogResponse; typedef DaemonPathsResponse = pb.DaemonPathsResponse; typedef UninstallModuleResponse = pb.UninstallModuleResponse; typedef FetchModuleDocsResponse = pb.FetchModuleDocsResponse; +typedef GetInstalledModuleDocsResponse = pb.GetInstalledModuleDocsResponse; typedef ListMcpClientsResponse = pb.ListMcpClientsResponse; typedef McpClientStatus = pb.McpClientStatus; typedef McpClientConfigEntry = pb.McpClientConfigEntry; @@ -240,6 +241,10 @@ class HubClient { /// when set, so Studio doesn't have to handle credentials. /// Errors come back inside [FetchModuleDocsResponse.errorKind] /// (not as exceptions) so the UI can render fallbacks. + /// + /// Deprecated: prefer [getInstalledModuleDocs], which reads + /// `MODULE.md`/`MODULE..md` shipped inside the bundle. + @Deprecated('Use getInstalledModuleDocs (reads from bundle, no network)') Future fetchModuleDocs( String name, { String locale = '', @@ -249,6 +254,25 @@ class HubClient { ); } + /// Read the installed module's `MODULE.md` (or `MODULE..md`) + /// from disk under `~/.fai/modules//`. No network. Works in + /// air-gapped installs. The response distinguishes three states via + /// [GetInstalledModuleDocsResponse.notInstalled] and an empty + /// [GetInstalledModuleDocsResponse.text]: + /// + /// - text non-empty → docs found, render them + /// - text empty + notInstalled=false → installed but bundle had + /// no inline docs + /// - text empty + notInstalled=true → module is not installed + Future getInstalledModuleDocs( + String name, { + String locale = '', + }) { + return _admin.getInstalledModuleDocs( + pb.GetInstalledModuleDocsRequest(name: name, locale: locale), + ); + } + /// List every configured MCP server with the last-discovery /// snapshot. Drives Studio's MCP-clients editor. Future listMcpClients() { @@ -489,27 +513,33 @@ class HubClient { /// flow that mixes text and binary inputs (e.g. extract /// taking a `document: bytes`) flows through one RPC: /// * [textInputs] — string values wrapped as text Payloads - /// * [fileInputs] — raw bytes wrapped as bytes Payloads, - /// with `application/octet-stream` MIME - /// type. Caller can override the MIME by - /// passing a [bytePayloads] entry instead. - /// Both maps default to empty so existing text-only callers + /// * [fileInputs] — raw bytes wrapped as bytes Payloads. + /// MIME type defaults to + /// `application/octet-stream`; clients that + /// know the real type pass it via + /// [fileMimeTypes] (same key). Modules + /// typically gate on MIME — e.g. + /// `text.extract` rejects octet-stream — so + /// accurate types matter. + /// All maps default to empty so existing text-only callers /// are backward-compatible. Keys must not collide between - /// the maps; on collision the bytes-shaped entry wins (the - /// less-common case is more likely to be the operator's - /// intent). + /// [textInputs] and [fileInputs]; on collision the bytes-shaped + /// entry wins (the less-common case is more likely to be the + /// operator's intent). Future runSavedFlow({ required String name, Map textInputs = const {}, Map fileInputs = const {}, + Map fileMimeTypes = const {}, }) async { final req = pb.RunSavedFlowRequest()..name = name; for (final entry in textInputs.entries) { req.inputs[entry.key] = pb_common.Payload()..text = entry.value; } for (final entry in fileInputs.entries) { + final mime = fileMimeTypes[entry.key] ?? 'application/octet-stream'; final bytes = pb_common.Bytes() - ..mimeType = 'application/octet-stream' + ..mimeType = mime ..data = entry.value; req.inputs[entry.key] = pb_common.Payload()..bytes = bytes; }