shortcutxl 0.3.50 → 0.3.51

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 (127) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/app/agent-session.d.ts +4 -0
  3. package/dist/app/agent-session.js +47 -10
  4. package/dist/app/approvals/types.d.ts +5 -0
  5. package/dist/app/auth/auth-storage.js +2 -7
  6. package/dist/app/extensions/slash-commands.js +1 -0
  7. package/dist/app/index.d.ts +2 -0
  8. package/dist/app/index.js +1 -0
  9. package/dist/app/local/tools/execute-code/observability.d.ts +4 -0
  10. package/dist/app/local/tools/execute-code/observability.js +57 -0
  11. package/dist/app/mcp/auth-status.d.ts +9 -1
  12. package/dist/app/mcp/auth-status.js +27 -16
  13. package/dist/app/mcp/catalog.d.ts +2 -1
  14. package/dist/app/mcp/catalog.js +15 -3
  15. package/dist/app/mcp/config.d.ts +14 -9
  16. package/dist/app/mcp/config.js +47 -61
  17. package/dist/app/mcp/connection.d.ts +1 -0
  18. package/dist/app/mcp/connection.js +100 -5
  19. package/dist/app/mcp/hosted-oauth-id.js +1 -4
  20. package/dist/app/mcp/hosted-oauth-provider.js +1 -1
  21. package/dist/app/mcp/index.js +24 -6
  22. package/dist/app/mcp/install.d.ts +21 -0
  23. package/dist/app/mcp/install.js +68 -0
  24. package/dist/app/mcp/oauth-discovery.d.ts +1 -0
  25. package/dist/app/mcp/oauth-discovery.js +45 -5
  26. package/dist/app/mcp/types.d.ts +2 -0
  27. package/dist/app/modes/action/agent.d.ts +3 -3
  28. package/dist/app/modes/action/agent.js +4 -9
  29. package/dist/app/modes/action/prompt.js +5 -5
  30. package/dist/app/modes/ask/agent.js +2 -1
  31. package/dist/app/modes/plan/agent.js +2 -1
  32. package/dist/app/modes/switch-mode.d.ts +8 -0
  33. package/dist/app/modes/switch-mode.js +9 -5
  34. package/dist/app/prompts/com-api-reference.json +215 -146
  35. package/dist/app/providers/model-resolver.d.ts +12 -0
  36. package/dist/app/providers/model-resolver.js +30 -0
  37. package/dist/app/providers/shortcut-stream.js +5 -1
  38. package/dist/app/session/permission-context.d.ts +3 -0
  39. package/dist/app/session/permission-context.js +4 -0
  40. package/dist/app/session/tool-registry.js +13 -2
  41. package/dist/app/settings-manager.js +3 -3
  42. package/dist/app/sync/skills-download.d.ts +16 -0
  43. package/dist/app/sync/skills-download.js +65 -6
  44. package/dist/app/sync/skills-status.d.ts +44 -0
  45. package/dist/app/sync/skills-status.js +279 -0
  46. package/dist/app/sync/skills-upload.d.ts +15 -0
  47. package/dist/app/sync/skills-upload.js +62 -6
  48. package/dist/app/tools/get-tool-info.js +1 -0
  49. package/dist/app/tools/index.d.ts +1 -0
  50. package/dist/app/tools/index.js +1 -0
  51. package/dist/app/tools/session-settings/get-session-settings.d.ts +11 -0
  52. package/dist/app/tools/session-settings/get-session-settings.js +36 -0
  53. package/dist/app/tools/session-settings/index.d.ts +3 -0
  54. package/dist/app/tools/session-settings/index.js +3 -0
  55. package/dist/app/tools/session-settings/update-session-settings.d.ts +15 -0
  56. package/dist/app/tools/session-settings/update-session-settings.js +77 -0
  57. package/dist/app/tools/switch-mode.d.ts +4 -3
  58. package/dist/app/tools/switch-mode.js +26 -8
  59. package/dist/app/tools/task/manager.js +0 -2
  60. package/dist/app/tools/task/send-message.js +10 -3
  61. package/dist/app/tools/task/task.js +4 -1
  62. package/dist/cli/args.js +3 -1
  63. package/dist/cli/mcp-commands.d.ts +2 -0
  64. package/dist/cli/mcp-commands.js +358 -0
  65. package/dist/cli.js +1083 -1061
  66. package/dist/config.js +2 -1
  67. package/dist/main.js +50 -58
  68. package/dist/model-ids.d.ts +4 -0
  69. package/dist/model-ids.js +3 -0
  70. package/dist/shared/files/lock-utils.d.ts +10 -0
  71. package/dist/shared/files/lock-utils.js +32 -0
  72. package/dist/shared/logging/agent-log.d.ts +1 -0
  73. package/dist/shared/logging/agent-log.js +4 -1
  74. package/dist/shared/platform/open.js +12 -5
  75. package/dist/shell/approvals/approval.d.ts +3 -19
  76. package/dist/shell/approvals/approval.js +2 -59
  77. package/dist/shell/approvals/excel-approval.d.ts +14 -3
  78. package/dist/shell/approvals/excel-approval.js +77 -7
  79. package/dist/shell/approvals/switch-mode-approval.d.ts +10 -0
  80. package/dist/shell/approvals/switch-mode-approval.js +35 -0
  81. package/dist/shell/approvals/task-spawn-approval.d.ts +6 -7
  82. package/dist/shell/approvals/task-spawn-approval.js +7 -29
  83. package/dist/shell/commands/mcp-command.d.ts +7 -1
  84. package/dist/shell/commands/mcp-command.js +93 -69
  85. package/dist/shell/components/interactive-shell-layout.d.ts +13 -2
  86. package/dist/shell/components/line-selection-scroll-container.d.ts +43 -0
  87. package/dist/shell/components/line-selection-scroll-container.js +245 -0
  88. package/dist/shell/components/primitives/footer.js +40 -11
  89. package/dist/shell/components/primitives/grid-table.d.ts +8 -1
  90. package/dist/shell/components/primitives/grid-table.js +45 -7
  91. package/dist/shell/export-html/canonical-trace-html.js +1 -1
  92. package/dist/shell/export-html/template.css +1124 -1110
  93. package/dist/shell/export-html/template.js +15 -11
  94. package/dist/shell/extension-ui.d.ts +1 -0
  95. package/dist/shell/index.d.ts +1 -0
  96. package/dist/shell/index.js +1 -0
  97. package/dist/shell/interactive/interactive-actions.d.ts +1 -0
  98. package/dist/shell/interactive/interactive-actions.js +1 -0
  99. package/dist/shell/interactive/interactive-mode-options.d.ts +8 -0
  100. package/dist/shell/interactive/interactive-mode.d.ts +18 -0
  101. package/dist/shell/interactive/interactive-mode.js +317 -11
  102. package/dist/shell/interactive/skills-workflow.d.ts +10 -2
  103. package/dist/shell/interactive/skills-workflow.js +66 -2
  104. package/dist/shell/keybindings.d.ts +1 -1
  105. package/dist/shell/keybindings.js +2 -0
  106. package/dist/shell/presentation/slash-command-rendering.js +2 -0
  107. package/dist/shell/session-client.d.ts +8 -0
  108. package/dist/shell/session-client.js +11 -1
  109. package/dist/shell/tools/presentation/registry.js +2 -2
  110. package/dist/shell/tools/presentation/renderers/tool-meta.d.ts +2 -2
  111. package/dist/shell/tools/presentation/renderers/tool-meta.js +4 -3
  112. package/dist/startup/interactive-commands.d.ts +8 -0
  113. package/dist/startup/interactive-commands.js +30 -0
  114. package/dist/tool-names.d.ts +2 -0
  115. package/dist/tool-names.js +2 -0
  116. package/dist/tui/terminal.js +2 -2
  117. package/package.json +1 -1
  118. package/skills/mcp/SKILL.md +94 -186
  119. package/user-docs/dist/index.html +13 -10
  120. package/user-docs/dist/shortcutxl-docs.pdf +0 -0
  121. package/xll/ShortcutXL.xll +0 -0
  122. package/xll/python/Lib/site-packages/httpx-0.28.1.dist-info/RECORD +1 -1
  123. package/xll/python/Lib/site-packages/pip-26.1.1.dist-info/RECORD +3 -3
  124. package/xll/python/Scripts/httpx.exe +0 -0
  125. package/xll/python/Scripts/pip.exe +0 -0
  126. package/xll/python/Scripts/pip3.12.exe +0 -0
  127. package/xll/python/Scripts/pip3.exe +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.51]
4
+
5
+ - **TUI rendering and text selection** - Improved TUI rendering, scrolling. Fixed text highlighting and selection for copy pastes.
6
+ - **Agentic MCP setup** - Agents can now help set up MCP servers through `shortcut mcp add/list/get/login/logout/remove`, with clearer login, logs, and OAuth settings.
7
+ - **User-requested session settings** - When asked, agents can inspect available models and thinking levels or apply your requested session change.
8
+ - **Skill sync clarity** - Skill sync now shows startup differences plus per-file upload/download changes, source, and visibility.
9
+ - **Fast mode toggle** - Fast Mode now shows in the footer and can be toggled with `alt+f` or `/fast`, with clearer availability warnings.
10
+ - **Scrollable approvals** - Large approval and permission dialogs now scroll properly, including spreadsheet change tables in small terminals.
11
+
3
12
  ## [0.3.50]
4
13
 
5
14
  - **MCP-enabled task agents** - General task agents can now use connected MCP servers, giving delegated work access to the same external tools and resources as the main CLI.
@@ -261,6 +261,10 @@ export declare class AgentSession {
261
261
  get model(): Model<any> | undefined;
262
262
  /** Current thinking level */
263
263
  get thinkingLevel(): ThinkingLevel;
264
+ get isFastModeEnabled(): boolean;
265
+ get isFastModeAvailable(): boolean;
266
+ setFastModeEnabled(enabled: boolean): void;
267
+ toggleFastMode(): boolean;
264
268
  /** Whether agent is currently streaming a response */
265
269
  get isStreaming(): boolean;
266
270
  /** Current effective system prompt (includes any per-turn extension modifications) */
@@ -16,6 +16,7 @@ import { isContextOverflow } from '@mariozechner/pi-ai';
16
16
  import { SessionState } from '../core/session-state.js';
17
17
  import { createCompactionActions, createErrorRecoveryActions, triggerCompactionIfNeeded } from '../core/session/compaction-bridge.js';
18
18
  import { SessionCompaction } from '../core/session/session-compaction.js';
19
+ import { isShortcutFastModeModel } from '../model-ids.js';
19
20
  import { formatFileUploadsContext, mergeFileUploads } from './file-uploads.js';
20
21
  import { expandPromptTemplate } from './resources/prompt-template-expansion.js';
21
22
  import { parseSkillBlock } from './resources/skill-block.js';
@@ -26,6 +27,7 @@ import { emitExtensionEvent } from './session/extension-emitter.js';
26
27
  import { ExtensionLifecycle } from './session/extension-lifecycle.js';
27
28
  import { ModelManager } from './session/model-manager.js';
28
29
  import { resolvePendingRefreshOverride } from './session/pending-refresh-overrides.js';
30
+ import { formatRuntimePermissionBypassContext } from './session/permission-context.js';
29
31
  import { PersistenceHandler } from './session/persistence-handler.js';
30
32
  import { assertNotExtensionCommand, buildMessageContent, normalizeUserContent, tryExtensionCommand } from './session/prompt-pipeline.js';
31
33
  import { QueueTracker } from './session/queue-tracker.js';
@@ -582,6 +584,24 @@ export class AgentSession {
582
584
  get thinkingLevel() {
583
585
  return this.agent.state.thinkingLevel;
584
586
  }
587
+ get isFastModeEnabled() {
588
+ return this.isFastModeAvailable && this.agent.speed === 'fast';
589
+ }
590
+ get isFastModeAvailable() {
591
+ return isShortcutFastModeModel(this.model);
592
+ }
593
+ setFastModeEnabled(enabled) {
594
+ this.agent.speed = enabled && this.isFastModeAvailable ? 'fast' : undefined;
595
+ }
596
+ toggleFastMode() {
597
+ if (!this.isFastModeAvailable) {
598
+ this.agent.speed = undefined;
599
+ return false;
600
+ }
601
+ const enabled = !this.isFastModeEnabled;
602
+ this.setFastModeEnabled(enabled);
603
+ return enabled;
604
+ }
585
605
  /** Whether agent is currently streaming a response */
586
606
  get isStreaming() {
587
607
  return this.agent.state.isStreaming;
@@ -792,18 +812,30 @@ export class AgentSession {
792
812
  timestamp: Date.now()
793
813
  });
794
814
  this.lastPrePromptDetails = undefined;
815
+ const prePromptContextParts = [formatRuntimePermissionBypassContext(this.permissionPolicy)];
816
+ let prePromptDetails;
795
817
  if (this._prePromptContext) {
796
818
  const ctx = await this._prePromptContext();
797
819
  if (ctx) {
798
820
  this.lastPrePromptDetails = ctx.details;
799
- messages.push({
800
- role: 'custom',
801
- customType: 'prePromptContext',
802
- content: ctx.text,
803
- display: false,
804
- details: ctx.details,
805
- timestamp: Date.now()
806
- });
821
+ prePromptDetails = ctx.details;
822
+ prePromptContextParts.unshift(ctx.text);
823
+ }
824
+ }
825
+ messages.push({
826
+ role: 'custom',
827
+ customType: 'prePromptContext',
828
+ content: prePromptContextParts.join('\n\n'),
829
+ display: false,
830
+ details: prePromptDetails,
831
+ timestamp: Date.now()
832
+ });
833
+ for (const message of messages) {
834
+ if (message.role === 'custom') {
835
+ this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
836
+ }
837
+ else if (message.role === 'user') {
838
+ this.sessionManager.appendMessage(message);
807
839
  }
808
840
  }
809
841
  // Inject any pending "nextTurn" messages as context alongside the user message
@@ -1149,11 +1181,16 @@ export class AgentSession {
1149
1181
  // =========================================================================
1150
1182
  /** Switch the session's active LLM to `model`, persisting the change. */
1151
1183
  async setModel(model) {
1152
- return this._models.setModel(model);
1184
+ await this._models.setModel(model);
1185
+ if (!this.isFastModeAvailable)
1186
+ this.setFastModeEnabled(false);
1153
1187
  }
1154
1188
  /** Cycle to the next configured model and report the switch result. */
1155
1189
  async cycleModel(direction = 'forward') {
1156
- return this._models.cycleModel(direction);
1190
+ const result = await this._models.cycleModel(direction);
1191
+ if (!this.isFastModeAvailable)
1192
+ this.setFastModeEnabled(false);
1193
+ return result;
1157
1194
  }
1158
1195
  /** Set the model's reasoning/thinking level for subsequent turns. */
1159
1196
  setThinkingLevel(level) {
@@ -66,4 +66,9 @@ export interface TaskSpawnApprovalData {
66
66
  /** Launch mode requested by the tool call before user approval overrides. */
67
67
  requestedRunInBackground: boolean;
68
68
  }
69
+ export interface SwitchModeApprovalData {
70
+ currentMode: string;
71
+ targetMode: string;
72
+ reason?: string;
73
+ }
69
74
  //# sourceMappingURL=types.d.ts.map
@@ -10,12 +10,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'f
10
10
  import { dirname, join } from 'path';
11
11
  import lockfile from 'proper-lockfile';
12
12
  import { getAgentDir } from '../../config.js';
13
- /**
14
- * Shared lock options so sync and async locks always use the same lock path.
15
- * realpath: false ensures both create the same .lock directory on Windows,
16
- * where fs.realpath can normalize paths differently.
17
- */
18
- const LOCK_BASE_OPTIONS = { realpath: false };
13
+ import { LOCK_BASE_OPTIONS, lockSyncWithRetries } from '../../shared/files/lock-utils.js';
19
14
  export class FileAuthStorageBackend {
20
15
  authPath;
21
16
  constructor(authPath = join(getAgentDir(), 'auth.json')) {
@@ -38,7 +33,7 @@ export class FileAuthStorageBackend {
38
33
  this.ensureFileExists();
39
34
  let release;
40
35
  try {
41
- release = lockfile.lockSync(this.authPath, LOCK_BASE_OPTIONS);
36
+ release = lockSyncWithRetries(this.authPath);
42
37
  const current = existsSync(this.authPath) ? readFileSync(this.authPath, 'utf-8') : undefined;
43
38
  const { result, next } = fn(current);
44
39
  if (next !== undefined) {
@@ -15,6 +15,7 @@ const ALL_BUILTIN_SLASH_COMMANDS = [
15
15
  // Model
16
16
  { name: 'model', description: 'Select model' },
17
17
  { name: 'thinking', description: 'Select thinking level' },
18
+ { name: 'fast', description: 'Toggle fast mode for subsequent turns' },
18
19
  { name: 'hide-thinking', description: 'Toggle visibility of thinking blocks' },
19
20
  // Checkpointing
20
21
  { name: 'resume', description: 'Resume a different session' },
@@ -48,6 +48,8 @@ export { readSkillProposalReviewSnapshot, type SkillProposalReviewSnapshot } fro
48
48
  export type { StartupInfoEntry } from './startup-info.js';
49
49
  export { downloadSkillsWithApproval } from './sync/skills-download.js';
50
50
  export type { SkillDownloadApprovalHandler, SkillDownloadApprovalRequest, SkillsDownloadResult } from './sync/skills-download.js';
51
+ export { checkSkillsSyncStatus } from './sync/skills-status.js';
52
+ export type { SkillSyncStatusItem, SkillsSyncStatus } from './sync/skills-status.js';
51
53
  export { uploadSkills } from './sync/skills-upload.js';
52
54
  export type { SkillUploadApprovalRequest, SkillsUploadResult } from './sync/skills-upload.js';
53
55
  export * from './tools/index.js';
package/dist/app/index.js CHANGED
@@ -37,6 +37,7 @@ export { default as skillProposalsExtension } from './skill-proposals/extension.
37
37
  export { getSkillProposalRoots } from './skill-proposals/proposal-fs.js';
38
38
  export { readSkillProposalReviewSnapshot } from './skill-proposals/review.js';
39
39
  export { downloadSkillsWithApproval } from './sync/skills-download.js';
40
+ export { checkSkillsSyncStatus } from './sync/skills-status.js';
40
41
  export { uploadSkills } from './sync/skills-upload.js';
41
42
  export * from './tools/index.js';
42
43
  export { buildWorkbookPromptContext, fetchWorkbookConnectionState, formatWorkbookScopePromptContext } from './workbook-context/workbook-summary.js';
@@ -0,0 +1,4 @@
1
+ import type { ShortcutClientLogger } from '../../../observability/index.js';
2
+ import type { ExcelExecApprovalObservability } from './executor.js';
3
+ export declare function createExecuteCodeApprovalObservability(clientLogger: ShortcutClientLogger): ExcelExecApprovalObservability;
4
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1,57 @@
1
+ const TOOL_NAME = 'execute_code';
2
+ export function createExecuteCodeApprovalObservability(clientLogger) {
3
+ return {
4
+ onRequested: (request) => {
5
+ clientLogger.info({
6
+ module: 'tool',
7
+ event: 'tool_approval_requested',
8
+ data: {
9
+ tool_name: TOOL_NAME,
10
+ approval_kind: request.payload.kind
11
+ }
12
+ });
13
+ },
14
+ onAccepted: (request, choice) => {
15
+ clientLogger.info({
16
+ module: 'tool',
17
+ event: 'tool_approval_accepted',
18
+ data: {
19
+ tool_name: TOOL_NAME,
20
+ approval_kind: request.payload.kind,
21
+ accept_all: choice === 'Accept All'
22
+ }
23
+ });
24
+ },
25
+ onRejected: (request) => {
26
+ clientLogger.warn({
27
+ module: 'tool',
28
+ event: 'tool_approval_rejected',
29
+ data: {
30
+ tool_name: TOOL_NAME,
31
+ approval_kind: request.payload.kind
32
+ }
33
+ });
34
+ },
35
+ onReverted: (request) => {
36
+ clientLogger.info({
37
+ module: 'tool',
38
+ event: 'tool_approval_reverted',
39
+ data: {
40
+ tool_name: TOOL_NAME,
41
+ approval_kind: request.payload.kind
42
+ }
43
+ });
44
+ },
45
+ onRevertFailed: (request) => {
46
+ clientLogger.error({
47
+ module: 'tool',
48
+ event: 'tool_approval_revert_failed',
49
+ data: {
50
+ tool_name: TOOL_NAME,
51
+ approval_kind: request.payload.kind
52
+ }
53
+ });
54
+ }
55
+ };
56
+ }
57
+ //# sourceMappingURL=observability.js.map
@@ -1,4 +1,12 @@
1
1
  import type { AuthStorage } from '../auth/auth-storage.js';
2
+ import type { McpOAuthStore } from './oauth-state-store.js';
2
3
  import { type McpAuthStatus, type McpServerConfig } from './types.js';
3
- export declare function getMcpAuthStatus(serverName: string, config: McpServerConfig, authStorage?: AuthStorage): Promise<McpAuthStatus>;
4
+ export type McpAuthDiscoveryMode = 'never' | 'cached' | 'network';
5
+ export interface ResolveMcpAuthStatusOptions {
6
+ authStorage?: AuthStorage;
7
+ mcpOAuthStore?: McpOAuthStore;
8
+ discovery?: McpAuthDiscoveryMode;
9
+ }
10
+ export declare function resolveMcpAuthStatus(serverName: string, config: McpServerConfig, options?: ResolveMcpAuthStatusOptions): Promise<McpAuthStatus>;
11
+ export declare function getMcpAuthStatus(serverName: string, config: McpServerConfig, authStorage?: AuthStorage, mcpOAuthStore?: McpOAuthStore): Promise<McpAuthStatus>;
4
12
  //# sourceMappingURL=auth-status.d.ts.map
@@ -1,33 +1,44 @@
1
1
  import { getSecret } from '../connectors/secret-store.js';
2
2
  import { getHostedMcpOAuthProviderId } from './hosted-oauth-id.js';
3
+ import { resolveHostedMcpOAuthTimeoutMs, supportsHostedMcpOAuth } from './oauth-discovery.js';
3
4
  import { isHostedServerConfig, isStdioServerConfig } from './types.js';
4
5
  function hasConfiguredHeaders(headers) {
5
6
  return !!headers && Object.keys(headers).length > 0;
6
7
  }
7
- export async function getMcpAuthStatus(serverName, config, authStorage) {
8
+ export async function resolveMcpAuthStatus(serverName, config, options = {}) {
8
9
  if (isStdioServerConfig(config)) {
9
10
  return 'unsupported';
10
11
  }
11
12
  const staticHeaders = { ...(config.headers ?? {}) };
12
13
  const secretHeaders = config.connection ? getSecret(config.connection) : undefined;
13
- if (isHostedServerConfig(config) && config.oauth) {
14
- if (!authStorage) {
15
- return hasConfiguredHeaders(staticHeaders) || hasConfiguredHeaders(secretHeaders)
16
- ? 'configured'
17
- : 'not_logged_in';
18
- }
19
- const credential = authStorage.get(getHostedMcpOAuthProviderId(serverName, config));
20
- if (credential?.type === 'oauth') {
14
+ if (hasConfiguredHeaders(staticHeaders) || hasConfiguredHeaders(secretHeaders)) {
15
+ return 'configured';
16
+ }
17
+ if (isHostedServerConfig(config)) {
18
+ const discovery = options.discovery ?? 'cached';
19
+ const providerId = getHostedMcpOAuthProviderId(serverName, config);
20
+ if (options.authStorage?.get(providerId)?.type === 'oauth') {
21
21
  return 'oauth';
22
22
  }
23
- return hasConfiguredHeaders(staticHeaders) || hasConfiguredHeaders(secretHeaders)
24
- ? 'configured'
25
- : 'not_logged_in';
26
- }
27
- if (Object.keys(staticHeaders).length > 0 ||
28
- (secretHeaders && Object.keys(secretHeaders).length > 0)) {
29
- return 'configured';
23
+ if (config.oauth ||
24
+ (discovery !== 'never' && options.mcpOAuthStore?.getDiscoveryState(providerId))) {
25
+ return 'not_logged_in';
26
+ }
27
+ if (discovery === 'network' &&
28
+ (await supportsHostedMcpOAuth(serverName, config, options.mcpOAuthStore, {
29
+ timeoutMs: resolveHostedMcpOAuthTimeoutMs(config)
30
+ }).catch(() => false))) {
31
+ return 'not_logged_in';
32
+ }
33
+ return 'unsupported';
30
34
  }
31
35
  return 'unsupported';
32
36
  }
37
+ export async function getMcpAuthStatus(serverName, config, authStorage, mcpOAuthStore) {
38
+ return resolveMcpAuthStatus(serverName, config, {
39
+ authStorage,
40
+ mcpOAuthStore,
41
+ discovery: 'cached'
42
+ });
43
+ }
33
44
  //# sourceMappingURL=auth-status.js.map
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Use mcp_describe(server) to get full tool details.
9
9
  */
10
- import type { McpAuthStatus, McpConnection, McpServerStatus } from './types.js';
10
+ import type { McpAuthStatus, McpConnection, McpFailureKind, McpServerStatus } from './types.js';
11
11
  /**
12
12
  * Build a compact tool catalog — just server names and tool names.
13
13
  * Detailed descriptions are available via mcp_describe.
@@ -21,6 +21,7 @@ export declare function buildStatusLine(entries: Array<{
21
21
  name: string;
22
22
  connected?: McpConnection;
23
23
  failure?: string;
24
+ failureKind?: McpFailureKind;
24
25
  authStatus: McpAuthStatus;
25
26
  transport: McpServerStatus['transport'];
26
27
  }>): string;
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Use mcp_describe(server) to get full tool details.
9
9
  */
10
+ import { getAgentLogPath } from '../../shared/logging/agent-log.js';
10
11
  /**
11
12
  * Build a compact tool catalog — just server names and tool names.
12
13
  * Detailed descriptions are available via mcp_describe.
@@ -41,7 +42,7 @@ function formatAuthSuffix(authStatus) {
41
42
  case 'oauth':
42
43
  return 'oauth';
43
44
  case 'not_logged_in':
44
- return 'login required';
45
+ return 'needs login';
45
46
  case 'unsupported':
46
47
  default:
47
48
  return 'no auth';
@@ -49,6 +50,7 @@ function formatAuthSuffix(authStatus) {
49
50
  }
50
51
  export function buildStatusLine(entries) {
51
52
  const parts = [];
53
+ let hasFailure = false;
52
54
  for (const entry of entries) {
53
55
  const authSuffix = entry.transport === 'streamable_http' && entry.authStatus !== 'unsupported'
54
56
  ? `, ${formatAuthSuffix(entry.authStatus)}`
@@ -57,12 +59,22 @@ export function buildStatusLine(entries) {
57
59
  parts.push(`${entry.name} (${entry.connected.tools.length} tools${authSuffix})`);
58
60
  }
59
61
  else if (entry.failure) {
60
- parts.push(`${entry.name} (failed${authSuffix})`);
62
+ hasFailure = true;
63
+ if (entry.failureKind === 'auth_required') {
64
+ parts.push(`${entry.name} (needs login - run /login -> MCP: ${entry.name})`);
65
+ }
66
+ else {
67
+ parts.push(`${entry.name} (failed)`);
68
+ }
61
69
  }
62
70
  else {
63
71
  parts.push(`${entry.name} (${formatAuthSuffix(entry.authStatus)})`);
64
72
  }
65
73
  }
66
- return parts.length > 0 ? `[MCP] ${parts.join(', ')}` : '';
74
+ if (parts.length === 0) {
75
+ return '';
76
+ }
77
+ const logSuffix = hasFailure ? `; logs: ${getAgentLogPath()}` : '';
78
+ return `[MCP] ${parts.join(', ')}${logSuffix}`;
67
79
  }
68
80
  //# sourceMappingURL=catalog.js.map
@@ -1,20 +1,25 @@
1
1
  /**
2
- * Load and merge MCP server configs from global + project paths.
2
+ * Load MCP server config from the global agent directory.
3
3
  *
4
- * Global: ~/.shortcut/agent/mcp.json
5
- * Project: .shortcut/mcp.json (in cwd)
6
- *
7
- * Project servers override global servers with the same name.
4
+ * MCPs are user-level integrations for ShortcutXL spreadsheet sessions, not
5
+ * project-local coding config.
8
6
  */
9
- import { type McpServerConfig } from './types.js';
7
+ import { type McpConfig, type McpServerConfig } from './types.js';
8
+ export declare function getMcpConfigPath(globalConfigDir: string): string;
9
+ export declare function readMcpConfigFile(path: string): McpConfig;
10
+ export declare function writeMcpConfigFile(path: string, config: McpConfig): void;
11
+ export declare function upsertMcpServerConfig(path: string, name: string, config: McpServerConfig, options?: {
12
+ force?: boolean;
13
+ }): void;
14
+ export declare function removeMcpServerConfig(path: string, name: string): boolean;
15
+ export declare function validateServerConfig(name: string, cfg: unknown): cfg is McpServerConfig;
16
+ export declare function normalizeServerConfig(name: string, cfg: McpServerConfig): McpServerConfig;
10
17
  interface LoadConfigOptions {
11
18
  cwd: string;
12
19
  globalConfigDir: string;
13
20
  }
14
21
  /**
15
- * Load and merge MCP server configs from global and project paths.
16
- * Project-local servers override global servers with the same name.
17
- * Warns on secrets found in project-local config.
22
+ * Load MCP server configs from the global agent path only.
18
23
  */
19
24
  export declare function loadMcpConfig(options: LoadConfigOptions): Record<string, McpServerConfig>;
20
25
  export {};
@@ -1,40 +1,61 @@
1
1
  /**
2
- * Load and merge MCP server configs from global + project paths.
2
+ * Load MCP server config from the global agent directory.
3
3
  *
4
- * Global: ~/.shortcut/agent/mcp.json
5
- * Project: .shortcut/mcp.json (in cwd)
6
- *
7
- * Project servers override global servers with the same name.
4
+ * MCPs are user-level integrations for ShortcutXL spreadsheet sessions, not
5
+ * project-local coding config.
8
6
  */
9
- import { existsSync, readFileSync } from 'fs';
10
- import { join } from 'path';
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
8
+ import { dirname, join } from 'path';
11
9
  import { log } from '../../shared/logging/agent-log.js';
12
10
  const TAG = 'mcp';
13
11
  const MCP_CONFIG_FILENAME = 'mcp.json';
14
- /** Patterns that look like secrets — warn if found in project-local config. */
15
- const SECRET_PATTERNS = [/^ghp_/, /^sk-/, /^xoxb-/, /^xoxp-/, /^gho_/, /^github_pat_/];
16
- function looksLikeSecret(value) {
17
- return SECRET_PATTERNS.some((p) => p.test(value));
12
+ export function getMcpConfigPath(globalConfigDir) {
13
+ return join(globalConfigDir, MCP_CONFIG_FILENAME);
18
14
  }
19
- function parseConfigFile(path) {
15
+ export function readMcpConfigFile(path) {
20
16
  if (!existsSync(path))
21
- return null;
17
+ return { mcpServers: {} };
22
18
  try {
23
19
  const raw = readFileSync(path, 'utf-8');
24
20
  const parsed = JSON.parse(raw);
25
21
  if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) {
26
22
  log.warn(TAG, `MCP config at ${path} missing "mcpServers" key — skipping.`);
27
- return null;
23
+ return { mcpServers: {} };
24
+ }
25
+ if (typeof parsed.mcpServers !== 'object' || Array.isArray(parsed.mcpServers)) {
26
+ log.warn(TAG, `MCP config at ${path} has invalid "mcpServers" shape — skipping.`);
27
+ return { mcpServers: {} };
28
28
  }
29
- return parsed;
29
+ return { mcpServers: parsed.mcpServers };
30
30
  }
31
31
  catch (err) {
32
32
  const msg = err instanceof SyntaxError ? err.message : String(err);
33
33
  log.warn(TAG, `Failed to parse ${path}: ${msg}`);
34
- return null;
34
+ return { mcpServers: {} };
35
+ }
36
+ }
37
+ export function writeMcpConfigFile(path, config) {
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n', 'utf-8');
40
+ }
41
+ export function upsertMcpServerConfig(path, name, config, options = {}) {
42
+ const existing = readMcpConfigFile(path);
43
+ if (!options.force && Object.prototype.hasOwnProperty.call(existing.mcpServers, name)) {
44
+ throw new Error(`MCP server '${name}' already exists in ${path}. Choose a different name or rerun with --force to replace it.`);
45
+ }
46
+ existing.mcpServers[name] = config;
47
+ writeMcpConfigFile(path, existing);
48
+ }
49
+ export function removeMcpServerConfig(path, name) {
50
+ const config = readMcpConfigFile(path);
51
+ if (!Object.prototype.hasOwnProperty.call(config.mcpServers, name)) {
52
+ return false;
35
53
  }
54
+ delete config.mcpServers[name];
55
+ writeMcpConfigFile(path, config);
56
+ return true;
36
57
  }
37
- function validateServerConfig(name, cfg) {
58
+ export function validateServerConfig(name, cfg) {
38
59
  if (!cfg || typeof cfg !== 'object') {
39
60
  log.warn(TAG, `MCP server '${name}': config must be an object — skipping.`);
40
61
  return false;
@@ -67,7 +88,7 @@ function normalizeBoolean(serverName, fieldName, value) {
67
88
  }
68
89
  return value;
69
90
  }
70
- function normalizeServerConfig(name, cfg) {
91
+ export function normalizeServerConfig(name, cfg) {
71
92
  const { timeout: legacyTimeout, startupTimeoutMs: rawStartupTimeoutMs, toolCallTimeoutMs: rawToolCallTimeoutMs, resetToolCallTimeoutOnProgress: rawResetToolCallTimeoutOnProgress, maxToolCallTotalTimeoutMs: rawMaxToolCallTotalTimeoutMs, ...rest } = cfg;
72
93
  const startupTimeoutMs = normalizePositiveInt(name, 'startupTimeoutMs', rawStartupTimeoutMs) ??
73
94
  normalizePositiveInt(name, 'timeout', legacyTimeout);
@@ -84,53 +105,18 @@ function normalizeServerConfig(name, cfg) {
84
105
  };
85
106
  return normalized;
86
107
  }
87
- function warnOnSecrets(path, config) {
88
- for (const [name, server] of Object.entries(config.mcpServers)) {
89
- const entries = [];
90
- if ('env' in server) {
91
- entries.push(...Object.entries(server.env ?? {}));
92
- }
93
- if ('headers' in server) {
94
- entries.push(...Object.entries(server.headers ?? {}));
95
- }
96
- for (const [key, value] of entries) {
97
- if (typeof value === 'string' && looksLikeSecret(value)) {
98
- log.warn(TAG, `MCP server '${name}': value '${key}' in ${path} looks like a secret. ` +
99
- `Consider moving it to the global config (~/.shortcut/agent/mcp.json) or using shell environment variables.`);
100
- return; // One warning per load is enough.
101
- }
102
- }
103
- }
104
- }
105
108
  /**
106
- * Load and merge MCP server configs from global and project paths.
107
- * Project-local servers override global servers with the same name.
108
- * Warns on secrets found in project-local config.
109
+ * Load MCP server configs from the global agent path only.
109
110
  */
110
111
  export function loadMcpConfig(options) {
111
- const globalPath = join(options.globalConfigDir, MCP_CONFIG_FILENAME);
112
- const projectPath = join(options.cwd, '.shortcut', MCP_CONFIG_FILENAME);
113
- const globalConfig = parseConfigFile(globalPath);
114
- const projectConfig = parseConfigFile(projectPath);
115
- if (projectConfig) {
116
- warnOnSecrets(projectPath, projectConfig);
117
- }
118
- // Merge: project overrides global.
119
- const merged = {};
120
- if (globalConfig) {
121
- for (const [name, cfg] of Object.entries(globalConfig.mcpServers)) {
122
- if (validateServerConfig(name, cfg)) {
123
- merged[name] = normalizeServerConfig(name, cfg);
124
- }
125
- }
126
- }
127
- if (projectConfig) {
128
- for (const [name, cfg] of Object.entries(projectConfig.mcpServers)) {
129
- if (validateServerConfig(name, cfg)) {
130
- merged[name] = normalizeServerConfig(name, cfg); // Override global
131
- }
112
+ const globalPath = getMcpConfigPath(options.globalConfigDir);
113
+ const globalConfig = readMcpConfigFile(globalPath);
114
+ const servers = {};
115
+ for (const [name, cfg] of Object.entries(globalConfig.mcpServers)) {
116
+ if (validateServerConfig(name, cfg)) {
117
+ servers[name] = normalizeServerConfig(name, cfg);
132
118
  }
133
119
  }
134
- return merged;
120
+ return servers;
135
121
  }
136
122
  //# sourceMappingURL=config.js.map
@@ -10,6 +10,7 @@ export declare class McpConnectionError extends Error {
10
10
  }
11
11
  export declare function resolveStartupTimeoutMs(config: McpServerConfig): number;
12
12
  export declare function buildRequestPolicy(config: McpServerConfig): McpRequestPolicy;
13
+ export declare function createHostedMcpLoggingFetch(serverName: string, fetchImpl?: typeof fetch): typeof fetch;
13
14
  /**
14
15
  * Connect a server, list tools, and return a live MCP connection.
15
16
  */