shortcutxl 0.2.12 → 0.2.13

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 (110) hide show
  1. package/README.md +26 -26
  2. package/agent-docs/README.md +397 -397
  3. package/agent-docs/docs/compaction.md +390 -390
  4. package/agent-docs/docs/custom-provider.md +580 -580
  5. package/agent-docs/docs/extensions.md +1971 -1971
  6. package/agent-docs/docs/packages.md +209 -209
  7. package/agent-docs/docs/rpc.md +1317 -1317
  8. package/agent-docs/docs/sdk.md +962 -962
  9. package/agent-docs/docs/session.md +412 -412
  10. package/agent-docs/docs/termux.md +127 -127
  11. package/agent-docs/docs/tui.md +887 -887
  12. package/agent-docs/examples/README.md +25 -25
  13. package/agent-docs/examples/extensions/README.md +205 -205
  14. package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
  15. package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
  16. package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
  17. package/agent-docs/examples/extensions/bookmark.ts +50 -50
  18. package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
  19. package/agent-docs/examples/extensions/claude-rules.ts +86 -86
  20. package/agent-docs/examples/extensions/commands.ts +75 -75
  21. package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
  22. package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
  23. package/agent-docs/examples/extensions/custom-footer.ts +63 -63
  24. package/agent-docs/examples/extensions/custom-header.ts +73 -73
  25. package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
  26. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
  27. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
  28. package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
  29. package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
  30. package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
  31. package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
  32. package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
  33. package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
  34. package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
  35. package/agent-docs/examples/extensions/event-bus.ts +43 -43
  36. package/agent-docs/examples/extensions/file-trigger.ts +41 -41
  37. package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
  38. package/agent-docs/examples/extensions/handoff.ts +155 -155
  39. package/agent-docs/examples/extensions/hello.ts +25 -25
  40. package/agent-docs/examples/extensions/inline-bash.ts +94 -94
  41. package/agent-docs/examples/extensions/input-transform.ts +43 -43
  42. package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
  43. package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
  44. package/agent-docs/examples/extensions/message-renderer.ts +59 -59
  45. package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
  46. package/agent-docs/examples/extensions/modal-editor.ts +90 -90
  47. package/agent-docs/examples/extensions/model-status.ts +31 -31
  48. package/agent-docs/examples/extensions/notify.ts +55 -55
  49. package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
  50. package/agent-docs/examples/extensions/overlay-test.ts +159 -159
  51. package/agent-docs/examples/extensions/permission-gate.ts +37 -37
  52. package/agent-docs/examples/extensions/pirate.ts +47 -47
  53. package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
  54. package/agent-docs/examples/extensions/preset.ts +418 -418
  55. package/agent-docs/examples/extensions/protected-paths.ts +30 -30
  56. package/agent-docs/examples/extensions/qna.ts +122 -122
  57. package/agent-docs/examples/extensions/question.ts +278 -278
  58. package/agent-docs/examples/extensions/questionnaire.ts +440 -440
  59. package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
  60. package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
  61. package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
  62. package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
  63. package/agent-docs/examples/extensions/send-user-message.ts +97 -97
  64. package/agent-docs/examples/extensions/session-name.ts +27 -27
  65. package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
  66. package/agent-docs/examples/extensions/snake.ts +343 -343
  67. package/agent-docs/examples/extensions/space-invaders.ts +566 -566
  68. package/agent-docs/examples/extensions/ssh.ts +233 -233
  69. package/agent-docs/examples/extensions/status-line.ts +40 -40
  70. package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
  71. package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
  72. package/agent-docs/examples/extensions/summarize.ts +206 -206
  73. package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
  74. package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
  75. package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
  76. package/agent-docs/examples/extensions/todo.ts +314 -314
  77. package/agent-docs/examples/extensions/tool-override.ts +146 -146
  78. package/agent-docs/examples/extensions/tools.ts +145 -145
  79. package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
  80. package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
  81. package/agent-docs/examples/extensions/widget-placement.ts +17 -17
  82. package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
  83. package/agent-docs/examples/rpc-extension-ui.ts +654 -654
  84. package/agent-docs/examples/sdk/01-minimal.ts +22 -22
  85. package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
  86. package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
  87. package/agent-docs/examples/sdk/04-skills.ts +53 -53
  88. package/agent-docs/examples/sdk/05-tools.ts +56 -56
  89. package/agent-docs/examples/sdk/06-extensions.ts +88 -88
  90. package/agent-docs/examples/sdk/07-context-files.ts +40 -40
  91. package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
  92. package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
  93. package/agent-docs/examples/sdk/10-settings.ts +54 -54
  94. package/agent-docs/examples/sdk/11-sessions.ts +48 -48
  95. package/agent-docs/examples/sdk/12-full-control.ts +82 -82
  96. package/agent-docs/examples/sdk/README.md +144 -144
  97. package/agent-docs/xll-spec.md +110 -110
  98. package/dist/core/auth-storage.js +21 -2
  99. package/package.json +1 -1
  100. package/xll/ShortcutXL.xll +0 -0
  101. package/xll/modules/debug_render.py +272 -272
  102. package/xll/modules/gameboy.py +241 -241
  103. package/xll/modules/pong.py +188 -188
  104. package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
  105. package/xll/modules/shortcut_xl/_log.py +12 -12
  106. package/xll/modules/shortcut_xl/_registry.py +44 -44
  107. package/xll/modules/stocks.py +100 -100
  108. /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
  109. /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
  110. /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
@@ -1,146 +1,146 @@
1
- /**
2
- * Tool Override Example - Demonstrates overriding built-in tools
3
- *
4
- * Extensions can register tools with the same name as built-in tools to replace them.
5
- * This is useful for:
6
- * - Adding logging or auditing to tool calls
7
- * - Implementing access control or sandboxing
8
- * - Routing tool calls to remote systems (e.g., shortcut-ssh-remote)
9
- * - Modifying tool behavior for specific workflows
10
- *
11
- * This example overrides the `read` tool to:
12
- * 1. Log all file access to a log file
13
- * 2. Block access to sensitive paths (e.g., .env files)
14
- * 3. Delegate to the original read implementation for allowed files
15
- *
16
- * Since no custom renderCall/renderResult are provided, the built-in renderer
17
- * is used automatically (syntax highlighting, line numbers, truncation warnings).
18
- *
19
- * Usage:
20
- * shortcut -e ./tool-override.ts
21
- */
22
-
23
- import { Type } from '@sinclair/typebox';
24
- import { appendFileSync, constants, readFileSync } from 'fs';
25
- import { access, readFile } from 'fs/promises';
26
- import { homedir } from 'os';
27
- import { join, resolve } from 'path';
28
- import type { ExtensionAPI, TextContent } from 'shortcutxl';
29
-
30
- const LOG_FILE = join(homedir(), '.shortcut', 'agent', 'read-access.log');
31
-
32
- // Paths that are blocked from reading
33
- const BLOCKED_PATTERNS = [
34
- /\.env$/,
35
- /\.env\..+$/,
36
- /secrets?\.(json|yaml|yml|toml)$/i,
37
- /credentials?\.(json|yaml|yml|toml)$/i,
38
- /\/\.ssh\//,
39
- /\/\.aws\//,
40
- /\/\.gnupg\//
41
- ];
42
-
43
- function isBlockedPath(path: string): boolean {
44
- return BLOCKED_PATTERNS.some((pattern) => pattern.test(path));
45
- }
46
-
47
- function logAccess(path: string, allowed: boolean, reason?: string) {
48
- const timestamp = new Date().toISOString();
49
- const status = allowed ? 'ALLOWED' : 'BLOCKED';
50
- const msg = reason ? ` (${reason})` : '';
51
- const line = `[${timestamp}] ${status}: ${path}${msg}\n`;
52
-
53
- try {
54
- appendFileSync(LOG_FILE, line);
55
- } catch {
56
- // Ignore logging errors
57
- }
58
- }
59
-
60
- const readSchema = Type.Object({
61
- path: Type.String({ description: 'Path to the file to read (relative or absolute)' }),
62
- offset: Type.Optional(
63
- Type.Number({ description: 'Line number to start reading from (1-indexed)' })
64
- ),
65
- limit: Type.Optional(Type.Number({ description: 'Maximum number of lines to read' }))
66
- });
67
-
68
- export default function (shortcut: ExtensionAPI) {
69
- shortcut.registerTool({
70
- name: 'read', // Same name as built-in - this will override it
71
- label: 'read (audited)',
72
- description:
73
- 'Read the contents of a file with access logging. Some sensitive paths (.env, secrets, credentials) are blocked.',
74
- parameters: readSchema,
75
-
76
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
77
- const { path, offset, limit } = params;
78
- const absolutePath = resolve(ctx.cwd, path);
79
-
80
- // Check if path is blocked
81
- if (isBlockedPath(absolutePath)) {
82
- logAccess(absolutePath, false, 'matches blocked pattern');
83
- return {
84
- content: [
85
- {
86
- type: 'text',
87
- text: `Access denied: "${path}" matches a blocked pattern (sensitive file). This tool blocks access to .env files, secrets, credentials, and SSH/AWS/GPG directories.`
88
- }
89
- ],
90
- details: { blocked: true }
91
- };
92
- }
93
-
94
- // Log allowed access
95
- logAccess(absolutePath, true);
96
-
97
- // Perform the actual read (simplified implementation)
98
- try {
99
- await access(absolutePath, constants.R_OK);
100
- const content = await readFile(absolutePath, 'utf-8');
101
- const lines = content.split('\n');
102
-
103
- // Apply offset and limit
104
- const startLine = offset ? Math.max(0, offset - 1) : 0;
105
- const endLine = limit ? startLine + limit : lines.length;
106
- const selectedLines = lines.slice(startLine, endLine);
107
-
108
- // Basic truncation (50KB limit)
109
- let text = selectedLines.join('\n');
110
- const maxBytes = 50 * 1024;
111
- if (Buffer.byteLength(text, 'utf-8') > maxBytes) {
112
- text = `${text.slice(0, maxBytes)}\n\n[Output truncated at 50KB]`;
113
- }
114
-
115
- return {
116
- content: [{ type: 'text', text }] as TextContent[],
117
- details: { lines: lines.length }
118
- };
119
- } catch (error: any) {
120
- return {
121
- content: [
122
- { type: 'text', text: `Error reading file: ${error.message}` }
123
- ] as TextContent[],
124
- details: { error: true }
125
- };
126
- }
127
- }
128
-
129
- // No renderCall/renderResult - uses built-in renderer automatically
130
- // (syntax highlighting, line numbers, truncation warnings, etc.)
131
- });
132
-
133
- // Also register a command to view the access log
134
- shortcut.registerCommand('read-log', {
135
- description: 'View the file access log',
136
- handler: async (_args, ctx) => {
137
- try {
138
- const log = readFileSync(LOG_FILE, 'utf-8');
139
- const lines = log.trim().split('\n').slice(-20); // Last 20 entries
140
- ctx.ui.notify(`Recent file access:\n${lines.join('\n')}`, 'info');
141
- } catch {
142
- ctx.ui.notify('No access log found', 'info');
143
- }
144
- }
145
- });
146
- }
1
+ /**
2
+ * Tool Override Example - Demonstrates overriding built-in tools
3
+ *
4
+ * Extensions can register tools with the same name as built-in tools to replace them.
5
+ * This is useful for:
6
+ * - Adding logging or auditing to tool calls
7
+ * - Implementing access control or sandboxing
8
+ * - Routing tool calls to remote systems (e.g., shortcut-ssh-remote)
9
+ * - Modifying tool behavior for specific workflows
10
+ *
11
+ * This example overrides the `read` tool to:
12
+ * 1. Log all file access to a log file
13
+ * 2. Block access to sensitive paths (e.g., .env files)
14
+ * 3. Delegate to the original read implementation for allowed files
15
+ *
16
+ * Since no custom renderCall/renderResult are provided, the built-in renderer
17
+ * is used automatically (syntax highlighting, line numbers, truncation warnings).
18
+ *
19
+ * Usage:
20
+ * shortcut -e ./tool-override.ts
21
+ */
22
+
23
+ import { Type } from '@sinclair/typebox';
24
+ import { appendFileSync, constants, readFileSync } from 'fs';
25
+ import { access, readFile } from 'fs/promises';
26
+ import { homedir } from 'os';
27
+ import { join, resolve } from 'path';
28
+ import type { ExtensionAPI, TextContent } from 'shortcutxl';
29
+
30
+ const LOG_FILE = join(homedir(), '.shortcut', 'agent', 'read-access.log');
31
+
32
+ // Paths that are blocked from reading
33
+ const BLOCKED_PATTERNS = [
34
+ /\.env$/,
35
+ /\.env\..+$/,
36
+ /secrets?\.(json|yaml|yml|toml)$/i,
37
+ /credentials?\.(json|yaml|yml|toml)$/i,
38
+ /\/\.ssh\//,
39
+ /\/\.aws\//,
40
+ /\/\.gnupg\//
41
+ ];
42
+
43
+ function isBlockedPath(path: string): boolean {
44
+ return BLOCKED_PATTERNS.some((pattern) => pattern.test(path));
45
+ }
46
+
47
+ function logAccess(path: string, allowed: boolean, reason?: string) {
48
+ const timestamp = new Date().toISOString();
49
+ const status = allowed ? 'ALLOWED' : 'BLOCKED';
50
+ const msg = reason ? ` (${reason})` : '';
51
+ const line = `[${timestamp}] ${status}: ${path}${msg}\n`;
52
+
53
+ try {
54
+ appendFileSync(LOG_FILE, line);
55
+ } catch {
56
+ // Ignore logging errors
57
+ }
58
+ }
59
+
60
+ const readSchema = Type.Object({
61
+ path: Type.String({ description: 'Path to the file to read (relative or absolute)' }),
62
+ offset: Type.Optional(
63
+ Type.Number({ description: 'Line number to start reading from (1-indexed)' })
64
+ ),
65
+ limit: Type.Optional(Type.Number({ description: 'Maximum number of lines to read' }))
66
+ });
67
+
68
+ export default function (shortcut: ExtensionAPI) {
69
+ shortcut.registerTool({
70
+ name: 'read', // Same name as built-in - this will override it
71
+ label: 'read (audited)',
72
+ description:
73
+ 'Read the contents of a file with access logging. Some sensitive paths (.env, secrets, credentials) are blocked.',
74
+ parameters: readSchema,
75
+
76
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
77
+ const { path, offset, limit } = params;
78
+ const absolutePath = resolve(ctx.cwd, path);
79
+
80
+ // Check if path is blocked
81
+ if (isBlockedPath(absolutePath)) {
82
+ logAccess(absolutePath, false, 'matches blocked pattern');
83
+ return {
84
+ content: [
85
+ {
86
+ type: 'text',
87
+ text: `Access denied: "${path}" matches a blocked pattern (sensitive file). This tool blocks access to .env files, secrets, credentials, and SSH/AWS/GPG directories.`
88
+ }
89
+ ],
90
+ details: { blocked: true }
91
+ };
92
+ }
93
+
94
+ // Log allowed access
95
+ logAccess(absolutePath, true);
96
+
97
+ // Perform the actual read (simplified implementation)
98
+ try {
99
+ await access(absolutePath, constants.R_OK);
100
+ const content = await readFile(absolutePath, 'utf-8');
101
+ const lines = content.split('\n');
102
+
103
+ // Apply offset and limit
104
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
105
+ const endLine = limit ? startLine + limit : lines.length;
106
+ const selectedLines = lines.slice(startLine, endLine);
107
+
108
+ // Basic truncation (50KB limit)
109
+ let text = selectedLines.join('\n');
110
+ const maxBytes = 50 * 1024;
111
+ if (Buffer.byteLength(text, 'utf-8') > maxBytes) {
112
+ text = `${text.slice(0, maxBytes)}\n\n[Output truncated at 50KB]`;
113
+ }
114
+
115
+ return {
116
+ content: [{ type: 'text', text }] as TextContent[],
117
+ details: { lines: lines.length }
118
+ };
119
+ } catch (error: any) {
120
+ return {
121
+ content: [
122
+ { type: 'text', text: `Error reading file: ${error.message}` }
123
+ ] as TextContent[],
124
+ details: { error: true }
125
+ };
126
+ }
127
+ }
128
+
129
+ // No renderCall/renderResult - uses built-in renderer automatically
130
+ // (syntax highlighting, line numbers, truncation warnings, etc.)
131
+ });
132
+
133
+ // Also register a command to view the access log
134
+ shortcut.registerCommand('read-log', {
135
+ description: 'View the file access log',
136
+ handler: async (_args, ctx) => {
137
+ try {
138
+ const log = readFileSync(LOG_FILE, 'utf-8');
139
+ const lines = log.trim().split('\n').slice(-20); // Last 20 entries
140
+ ctx.ui.notify(`Recent file access:\n${lines.join('\n')}`, 'info');
141
+ } catch {
142
+ ctx.ui.notify('No access log found', 'info');
143
+ }
144
+ }
145
+ });
146
+ }
@@ -1,145 +1,145 @@
1
- /**
2
- * Tools Extension
3
- *
4
- * Provides a /tools command to enable/disable tools interactively.
5
- * Tool selection persists across session reloads and respects branch navigation.
6
- *
7
- * Usage:
8
- * 1. Copy this file to ~/.shortcut/agent/extensions/ or your project's .shortcut/extensions/
9
- * 2. Use /tools to open the tool selector
10
- */
11
-
12
- import type { ExtensionAPI, ExtensionContext, ToolInfo } from 'shortcutxl';
13
- import { Container, getSettingsListTheme, type SettingItem, SettingsList } from 'shortcutxl';
14
-
15
- // State persisted to session
16
- interface ToolsState {
17
- enabledTools: string[];
18
- }
19
-
20
- export default function toolsExtension(shortcut: ExtensionAPI) {
21
- // Track enabled tools
22
- let enabledTools: Set<string> = new Set();
23
- let allTools: ToolInfo[] = [];
24
-
25
- // Persist current state
26
- function persistState() {
27
- shortcut.appendEntry<ToolsState>('tools-config', {
28
- enabledTools: Array.from(enabledTools)
29
- });
30
- }
31
-
32
- // Apply current tool selection
33
- function applyTools() {
34
- shortcut.setActiveTools(Array.from(enabledTools));
35
- }
36
-
37
- // Find the last tools-config entry in the current branch
38
- function restoreFromBranch(ctx: ExtensionContext) {
39
- allTools = shortcut.getAllTools();
40
-
41
- // Get entries in current branch only
42
- const branchEntries = ctx.sessionManager.getBranch();
43
- let savedTools: string[] | undefined;
44
-
45
- for (const entry of branchEntries) {
46
- if (entry.type === 'custom' && entry.customType === 'tools-config') {
47
- const data = entry.data as ToolsState | undefined;
48
- if (data?.enabledTools) {
49
- savedTools = data.enabledTools;
50
- }
51
- }
52
- }
53
-
54
- if (savedTools) {
55
- // Restore saved tool selection (filter to only tools that still exist)
56
- const allToolNames = allTools.map((t) => t.name);
57
- enabledTools = new Set(savedTools.filter((t: string) => allToolNames.includes(t)));
58
- applyTools();
59
- } else {
60
- // No saved state - sync with currently active tools
61
- enabledTools = new Set(shortcut.getActiveTools());
62
- }
63
- }
64
-
65
- // Register /tools command
66
- shortcut.registerCommand('tools', {
67
- description: 'Enable/disable tools',
68
- handler: async (_args, ctx) => {
69
- // Refresh tool list
70
- allTools = shortcut.getAllTools();
71
-
72
- await ctx.ui.custom((tui, theme, _kb, done) => {
73
- // Build settings items for each tool
74
- const items: SettingItem[] = allTools.map((tool) => ({
75
- id: tool.name,
76
- label: tool.name,
77
- currentValue: enabledTools.has(tool.name) ? 'enabled' : 'disabled',
78
- values: ['enabled', 'disabled']
79
- }));
80
-
81
- const container = new Container();
82
- container.addChild(
83
- new (class {
84
- render(_width: number) {
85
- return [theme.fg('accent', theme.bold('Tool Configuration')), ''];
86
- }
87
- invalidate() {}
88
- })()
89
- );
90
-
91
- const settingsList = new SettingsList(
92
- items,
93
- Math.min(items.length + 2, 15),
94
- getSettingsListTheme(),
95
- (id, newValue) => {
96
- // Update enabled state and apply immediately
97
- if (newValue === 'enabled') {
98
- enabledTools.add(id);
99
- } else {
100
- enabledTools.delete(id);
101
- }
102
- applyTools();
103
- persistState();
104
- },
105
- () => {
106
- // Close dialog
107
- done(undefined);
108
- }
109
- );
110
-
111
- container.addChild(settingsList);
112
-
113
- const component = {
114
- render(width: number) {
115
- return container.render(width);
116
- },
117
- invalidate() {
118
- container.invalidate();
119
- },
120
- handleInput(data: string) {
121
- settingsList.handleInput?.(data);
122
- tui.requestRender();
123
- }
124
- };
125
-
126
- return component;
127
- });
128
- }
129
- });
130
-
131
- // Restore state on session start
132
- shortcut.on('session_start', async (_event, ctx) => {
133
- restoreFromBranch(ctx);
134
- });
135
-
136
- // Restore state when navigating the session tree
137
- shortcut.on('session_tree', async (_event, ctx) => {
138
- restoreFromBranch(ctx);
139
- });
140
-
141
- // Restore state after forking
142
- shortcut.on('session_fork', async (_event, ctx) => {
143
- restoreFromBranch(ctx);
144
- });
145
- }
1
+ /**
2
+ * Tools Extension
3
+ *
4
+ * Provides a /tools command to enable/disable tools interactively.
5
+ * Tool selection persists across session reloads and respects branch navigation.
6
+ *
7
+ * Usage:
8
+ * 1. Copy this file to ~/.shortcut/agent/extensions/ or your project's .shortcut/extensions/
9
+ * 2. Use /tools to open the tool selector
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionContext, ToolInfo } from 'shortcutxl';
13
+ import { Container, getSettingsListTheme, type SettingItem, SettingsList } from 'shortcutxl';
14
+
15
+ // State persisted to session
16
+ interface ToolsState {
17
+ enabledTools: string[];
18
+ }
19
+
20
+ export default function toolsExtension(shortcut: ExtensionAPI) {
21
+ // Track enabled tools
22
+ let enabledTools: Set<string> = new Set();
23
+ let allTools: ToolInfo[] = [];
24
+
25
+ // Persist current state
26
+ function persistState() {
27
+ shortcut.appendEntry<ToolsState>('tools-config', {
28
+ enabledTools: Array.from(enabledTools)
29
+ });
30
+ }
31
+
32
+ // Apply current tool selection
33
+ function applyTools() {
34
+ shortcut.setActiveTools(Array.from(enabledTools));
35
+ }
36
+
37
+ // Find the last tools-config entry in the current branch
38
+ function restoreFromBranch(ctx: ExtensionContext) {
39
+ allTools = shortcut.getAllTools();
40
+
41
+ // Get entries in current branch only
42
+ const branchEntries = ctx.sessionManager.getBranch();
43
+ let savedTools: string[] | undefined;
44
+
45
+ for (const entry of branchEntries) {
46
+ if (entry.type === 'custom' && entry.customType === 'tools-config') {
47
+ const data = entry.data as ToolsState | undefined;
48
+ if (data?.enabledTools) {
49
+ savedTools = data.enabledTools;
50
+ }
51
+ }
52
+ }
53
+
54
+ if (savedTools) {
55
+ // Restore saved tool selection (filter to only tools that still exist)
56
+ const allToolNames = allTools.map((t) => t.name);
57
+ enabledTools = new Set(savedTools.filter((t: string) => allToolNames.includes(t)));
58
+ applyTools();
59
+ } else {
60
+ // No saved state - sync with currently active tools
61
+ enabledTools = new Set(shortcut.getActiveTools());
62
+ }
63
+ }
64
+
65
+ // Register /tools command
66
+ shortcut.registerCommand('tools', {
67
+ description: 'Enable/disable tools',
68
+ handler: async (_args, ctx) => {
69
+ // Refresh tool list
70
+ allTools = shortcut.getAllTools();
71
+
72
+ await ctx.ui.custom((tui, theme, _kb, done) => {
73
+ // Build settings items for each tool
74
+ const items: SettingItem[] = allTools.map((tool) => ({
75
+ id: tool.name,
76
+ label: tool.name,
77
+ currentValue: enabledTools.has(tool.name) ? 'enabled' : 'disabled',
78
+ values: ['enabled', 'disabled']
79
+ }));
80
+
81
+ const container = new Container();
82
+ container.addChild(
83
+ new (class {
84
+ render(_width: number) {
85
+ return [theme.fg('accent', theme.bold('Tool Configuration')), ''];
86
+ }
87
+ invalidate() {}
88
+ })()
89
+ );
90
+
91
+ const settingsList = new SettingsList(
92
+ items,
93
+ Math.min(items.length + 2, 15),
94
+ getSettingsListTheme(),
95
+ (id, newValue) => {
96
+ // Update enabled state and apply immediately
97
+ if (newValue === 'enabled') {
98
+ enabledTools.add(id);
99
+ } else {
100
+ enabledTools.delete(id);
101
+ }
102
+ applyTools();
103
+ persistState();
104
+ },
105
+ () => {
106
+ // Close dialog
107
+ done(undefined);
108
+ }
109
+ );
110
+
111
+ container.addChild(settingsList);
112
+
113
+ const component = {
114
+ render(width: number) {
115
+ return container.render(width);
116
+ },
117
+ invalidate() {
118
+ container.invalidate();
119
+ },
120
+ handleInput(data: string) {
121
+ settingsList.handleInput?.(data);
122
+ tui.requestRender();
123
+ }
124
+ };
125
+
126
+ return component;
127
+ });
128
+ }
129
+ });
130
+
131
+ // Restore state on session start
132
+ shortcut.on('session_start', async (_event, ctx) => {
133
+ restoreFromBranch(ctx);
134
+ });
135
+
136
+ // Restore state when navigating the session tree
137
+ shortcut.on('session_tree', async (_event, ctx) => {
138
+ restoreFromBranch(ctx);
139
+ });
140
+
141
+ // Restore state after forking
142
+ shortcut.on('session_fork', async (_event, ctx) => {
143
+ restoreFromBranch(ctx);
144
+ });
145
+ }
@@ -1,40 +1,40 @@
1
- import type { ExtensionAPI, ExtensionContext } from 'shortcutxl';
2
-
3
- const COMPACT_THRESHOLD_TOKENS = 100_000;
4
-
5
- export default function (shortcut: ExtensionAPI) {
6
- const triggerCompaction = (ctx: ExtensionContext, customInstructions?: string) => {
7
- if (ctx.hasUI) {
8
- ctx.ui.notify('Compaction started', 'info');
9
- }
10
- ctx.compact({
11
- customInstructions,
12
- onComplete: () => {
13
- if (ctx.hasUI) {
14
- ctx.ui.notify('Compaction completed', 'info');
15
- }
16
- },
17
- onError: (error) => {
18
- if (ctx.hasUI) {
19
- ctx.ui.notify(`Compaction failed: ${error.message}`, 'error');
20
- }
21
- }
22
- });
23
- };
24
-
25
- shortcut.on('turn_end', (_event, ctx) => {
26
- const usage = ctx.getContextUsage();
27
- if (!usage || usage.tokens === null || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
28
- return;
29
- }
30
- triggerCompaction(ctx);
31
- });
32
-
33
- shortcut.registerCommand('trigger-compact', {
34
- description: 'Trigger compaction immediately',
35
- handler: async (args, ctx) => {
36
- const instructions = args.trim() || undefined;
37
- triggerCompaction(ctx, instructions);
38
- }
39
- });
40
- }
1
+ import type { ExtensionAPI, ExtensionContext } from 'shortcutxl';
2
+
3
+ const COMPACT_THRESHOLD_TOKENS = 100_000;
4
+
5
+ export default function (shortcut: ExtensionAPI) {
6
+ const triggerCompaction = (ctx: ExtensionContext, customInstructions?: string) => {
7
+ if (ctx.hasUI) {
8
+ ctx.ui.notify('Compaction started', 'info');
9
+ }
10
+ ctx.compact({
11
+ customInstructions,
12
+ onComplete: () => {
13
+ if (ctx.hasUI) {
14
+ ctx.ui.notify('Compaction completed', 'info');
15
+ }
16
+ },
17
+ onError: (error) => {
18
+ if (ctx.hasUI) {
19
+ ctx.ui.notify(`Compaction failed: ${error.message}`, 'error');
20
+ }
21
+ }
22
+ });
23
+ };
24
+
25
+ shortcut.on('turn_end', (_event, ctx) => {
26
+ const usage = ctx.getContextUsage();
27
+ if (!usage || usage.tokens === null || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
28
+ return;
29
+ }
30
+ triggerCompaction(ctx);
31
+ });
32
+
33
+ shortcut.registerCommand('trigger-compact', {
34
+ description: 'Trigger compaction immediately',
35
+ handler: async (args, ctx) => {
36
+ const instructions = args.trim() || undefined;
37
+ triggerCompaction(ctx, instructions);
38
+ }
39
+ });
40
+ }