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,155 +1,155 @@
1
- /**
2
- * Handoff extension - transfer context to a new focused session
3
- *
4
- * Instead of compacting (which is lossy), handoff extracts what matters
5
- * for your next task and creates a new session with a generated prompt.
6
- *
7
- * Usage:
8
- * /handoff now implement this for teams as well
9
- * /handoff execute phase one of the plan
10
- * /handoff check other places that need this fix
11
- *
12
- * The generated prompt appears as a draft in the editor for review/editing.
13
- */
14
-
15
- import type { ExtensionAPI, SessionEntry } from 'shortcutxl';
16
- import {
17
- BorderedLoader,
18
- complete,
19
- convertToLlm,
20
- serializeConversation,
21
- type Message
22
- } from 'shortcutxl';
23
-
24
- const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
25
-
26
- 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
27
- 2. Lists any relevant files that were discussed or modified
28
- 3. Clearly states the next task based on the user's goal
29
- 4. Is self-contained - the new thread should be able to proceed without the old conversation
30
-
31
- Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
32
-
33
- Example output format:
34
- ## Context
35
- We've been working on X. Key decisions:
36
- - Decision 1
37
- - Decision 2
38
-
39
- Files involved:
40
- - path/to/file1.ts
41
- - path/to/file2.ts
42
-
43
- ## Task
44
- [Clear description of what to do next based on user's goal]`;
45
-
46
- export default function (shortcut: ExtensionAPI) {
47
- shortcut.registerCommand('handoff', {
48
- description: 'Transfer context to a new focused session',
49
- handler: async (args, ctx) => {
50
- if (!ctx.hasUI) {
51
- ctx.ui.notify('handoff requires interactive mode', 'error');
52
- return;
53
- }
54
-
55
- if (!ctx.model) {
56
- ctx.ui.notify('No model selected', 'error');
57
- return;
58
- }
59
-
60
- const goal = args.trim();
61
- if (!goal) {
62
- ctx.ui.notify('Usage: /handoff <goal for new thread>', 'error');
63
- return;
64
- }
65
-
66
- // Gather conversation context from current branch
67
- const branch = ctx.sessionManager.getBranch();
68
- const messages = branch
69
- .filter((entry): entry is SessionEntry & { type: 'message' } => entry.type === 'message')
70
- .map((entry) => entry.message);
71
-
72
- if (messages.length === 0) {
73
- ctx.ui.notify('No conversation to hand off', 'error');
74
- return;
75
- }
76
-
77
- // Convert to LLM format and serialize
78
- const llmMessages = convertToLlm(messages);
79
- const conversationText = serializeConversation(llmMessages);
80
- const currentSessionFile = ctx.sessionManager.getSessionFile();
81
-
82
- // Generate the handoff prompt with loader UI
83
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
84
- const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
85
- loader.onAbort = () => done(null);
86
-
87
- const doGenerate = async () => {
88
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
89
-
90
- const userMessage: Message = {
91
- role: 'user',
92
- content: [
93
- {
94
- type: 'text',
95
- text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`
96
- }
97
- ],
98
- timestamp: Date.now()
99
- };
100
-
101
- const response = await complete(
102
- ctx.model!,
103
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
104
- { apiKey, signal: loader.signal }
105
- );
106
-
107
- if (response.stopReason === 'aborted') {
108
- return null;
109
- }
110
-
111
- return response.content
112
- .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
113
- .map((c) => c.text)
114
- .join('\n');
115
- };
116
-
117
- doGenerate()
118
- .then(done)
119
- .catch((err) => {
120
- console.error('Handoff generation failed:', err);
121
- done(null);
122
- });
123
-
124
- return loader;
125
- });
126
-
127
- if (result === null) {
128
- ctx.ui.notify('Cancelled', 'info');
129
- return;
130
- }
131
-
132
- // Let user edit the generated prompt
133
- const editedPrompt = await ctx.ui.editor('Edit handoff prompt', result);
134
-
135
- if (editedPrompt === undefined) {
136
- ctx.ui.notify('Cancelled', 'info');
137
- return;
138
- }
139
-
140
- // Create new session with parent tracking
141
- const newSessionResult = await ctx.newSession({
142
- parentSession: currentSessionFile
143
- });
144
-
145
- if (newSessionResult.cancelled) {
146
- ctx.ui.notify('New session cancelled', 'info');
147
- return;
148
- }
149
-
150
- // Set the edited prompt in the main editor for submission
151
- ctx.ui.setEditorText(editedPrompt);
152
- ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
153
- }
154
- });
155
- }
1
+ /**
2
+ * Handoff extension - transfer context to a new focused session
3
+ *
4
+ * Instead of compacting (which is lossy), handoff extracts what matters
5
+ * for your next task and creates a new session with a generated prompt.
6
+ *
7
+ * Usage:
8
+ * /handoff now implement this for teams as well
9
+ * /handoff execute phase one of the plan
10
+ * /handoff check other places that need this fix
11
+ *
12
+ * The generated prompt appears as a draft in the editor for review/editing.
13
+ */
14
+
15
+ import type { ExtensionAPI, SessionEntry } from 'shortcutxl';
16
+ import {
17
+ BorderedLoader,
18
+ complete,
19
+ convertToLlm,
20
+ serializeConversation,
21
+ type Message
22
+ } from 'shortcutxl';
23
+
24
+ const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
25
+
26
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
27
+ 2. Lists any relevant files that were discussed or modified
28
+ 3. Clearly states the next task based on the user's goal
29
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
30
+
31
+ Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
32
+
33
+ Example output format:
34
+ ## Context
35
+ We've been working on X. Key decisions:
36
+ - Decision 1
37
+ - Decision 2
38
+
39
+ Files involved:
40
+ - path/to/file1.ts
41
+ - path/to/file2.ts
42
+
43
+ ## Task
44
+ [Clear description of what to do next based on user's goal]`;
45
+
46
+ export default function (shortcut: ExtensionAPI) {
47
+ shortcut.registerCommand('handoff', {
48
+ description: 'Transfer context to a new focused session',
49
+ handler: async (args, ctx) => {
50
+ if (!ctx.hasUI) {
51
+ ctx.ui.notify('handoff requires interactive mode', 'error');
52
+ return;
53
+ }
54
+
55
+ if (!ctx.model) {
56
+ ctx.ui.notify('No model selected', 'error');
57
+ return;
58
+ }
59
+
60
+ const goal = args.trim();
61
+ if (!goal) {
62
+ ctx.ui.notify('Usage: /handoff <goal for new thread>', 'error');
63
+ return;
64
+ }
65
+
66
+ // Gather conversation context from current branch
67
+ const branch = ctx.sessionManager.getBranch();
68
+ const messages = branch
69
+ .filter((entry): entry is SessionEntry & { type: 'message' } => entry.type === 'message')
70
+ .map((entry) => entry.message);
71
+
72
+ if (messages.length === 0) {
73
+ ctx.ui.notify('No conversation to hand off', 'error');
74
+ return;
75
+ }
76
+
77
+ // Convert to LLM format and serialize
78
+ const llmMessages = convertToLlm(messages);
79
+ const conversationText = serializeConversation(llmMessages);
80
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
81
+
82
+ // Generate the handoff prompt with loader UI
83
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
84
+ const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
85
+ loader.onAbort = () => done(null);
86
+
87
+ const doGenerate = async () => {
88
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
89
+
90
+ const userMessage: Message = {
91
+ role: 'user',
92
+ content: [
93
+ {
94
+ type: 'text',
95
+ text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`
96
+ }
97
+ ],
98
+ timestamp: Date.now()
99
+ };
100
+
101
+ const response = await complete(
102
+ ctx.model!,
103
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
104
+ { apiKey, signal: loader.signal }
105
+ );
106
+
107
+ if (response.stopReason === 'aborted') {
108
+ return null;
109
+ }
110
+
111
+ return response.content
112
+ .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
113
+ .map((c) => c.text)
114
+ .join('\n');
115
+ };
116
+
117
+ doGenerate()
118
+ .then(done)
119
+ .catch((err) => {
120
+ console.error('Handoff generation failed:', err);
121
+ done(null);
122
+ });
123
+
124
+ return loader;
125
+ });
126
+
127
+ if (result === null) {
128
+ ctx.ui.notify('Cancelled', 'info');
129
+ return;
130
+ }
131
+
132
+ // Let user edit the generated prompt
133
+ const editedPrompt = await ctx.ui.editor('Edit handoff prompt', result);
134
+
135
+ if (editedPrompt === undefined) {
136
+ ctx.ui.notify('Cancelled', 'info');
137
+ return;
138
+ }
139
+
140
+ // Create new session with parent tracking
141
+ const newSessionResult = await ctx.newSession({
142
+ parentSession: currentSessionFile
143
+ });
144
+
145
+ if (newSessionResult.cancelled) {
146
+ ctx.ui.notify('New session cancelled', 'info');
147
+ return;
148
+ }
149
+
150
+ // Set the edited prompt in the main editor for submission
151
+ ctx.ui.setEditorText(editedPrompt);
152
+ ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
153
+ }
154
+ });
155
+ }
@@ -1,25 +1,25 @@
1
- /**
2
- * Hello Tool - Minimal custom tool example
3
- */
4
-
5
- import type { ExtensionAPI } from 'shortcutxl';
6
- import { Type } from 'shortcutxl';
7
-
8
- export default function (shortcut: ExtensionAPI) {
9
- shortcut.registerTool({
10
- name: 'hello',
11
- label: 'Hello',
12
- description: 'A simple greeting tool',
13
- parameters: Type.Object({
14
- name: Type.String({ description: 'Name to greet' })
15
- }),
16
-
17
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
18
- const { name } = params as { name: string };
19
- return {
20
- content: [{ type: 'text', text: `Hello, ${name}!` }],
21
- details: { greeted: name }
22
- };
23
- }
24
- });
25
- }
1
+ /**
2
+ * Hello Tool - Minimal custom tool example
3
+ */
4
+
5
+ import type { ExtensionAPI } from 'shortcutxl';
6
+ import { Type } from 'shortcutxl';
7
+
8
+ export default function (shortcut: ExtensionAPI) {
9
+ shortcut.registerTool({
10
+ name: 'hello',
11
+ label: 'Hello',
12
+ description: 'A simple greeting tool',
13
+ parameters: Type.Object({
14
+ name: Type.String({ description: 'Name to greet' })
15
+ }),
16
+
17
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
18
+ const { name } = params as { name: string };
19
+ return {
20
+ content: [{ type: 'text', text: `Hello, ${name}!` }],
21
+ details: { greeted: name }
22
+ };
23
+ }
24
+ });
25
+ }
@@ -1,94 +1,94 @@
1
- /**
2
- * Inline Bash Extension - expands inline bash commands in user prompts.
3
- *
4
- * Start shortcut with this extension:
5
- * shortcut -e ./examples/extensions/inline-bash.ts
6
- *
7
- * Then type prompts with inline bash:
8
- * What's in !{pwd}?
9
- * The current branch is !{git branch --show-current} and status: !{git status --short}
10
- * My node version is !{node --version}
11
- *
12
- * The !{command} patterns are executed and replaced with their output before
13
- * the prompt is sent to the agent.
14
- *
15
- * Note: Regular !command syntax (whole-line bash) is preserved and works as before.
16
- */
17
- import type { ExtensionAPI } from 'shortcutxl';
18
-
19
- export default function (shortcut: ExtensionAPI) {
20
- const PATTERN = /!\{([^}]+)\}/g;
21
- const TIMEOUT_MS = 30000;
22
-
23
- shortcut.on('input', async (event, ctx) => {
24
- const text = event.text;
25
-
26
- // Don't process if it's a whole-line bash command (starts with !)
27
- // This preserves the existing !command behavior
28
- if (text.trimStart().startsWith('!') && !text.trimStart().startsWith('!{')) {
29
- return { action: 'continue' };
30
- }
31
-
32
- // Check if there are any inline bash patterns
33
- if (!PATTERN.test(text)) {
34
- return { action: 'continue' };
35
- }
36
-
37
- // Reset regex state after test()
38
- PATTERN.lastIndex = 0;
39
-
40
- let result = text;
41
- const expansions: Array<{ command: string; output: string; error?: string }> = [];
42
-
43
- // Find all matches first (to avoid issues with replacing while iterating)
44
- const matches: Array<{ full: string; command: string }> = [];
45
- let match = PATTERN.exec(text);
46
- while (match) {
47
- matches.push({ full: match[0], command: match[1] });
48
- match = PATTERN.exec(text);
49
- }
50
-
51
- // Execute each command and collect results
52
- for (const { full, command } of matches) {
53
- try {
54
- const bashResult = await shortcut.exec('bash', ['-c', command], {
55
- timeout: TIMEOUT_MS
56
- });
57
-
58
- const output = bashResult.stdout || bashResult.stderr || '';
59
- const trimmed = output.trim();
60
-
61
- if (bashResult.code !== 0 && bashResult.stderr) {
62
- expansions.push({
63
- command,
64
- output: trimmed,
65
- error: `exit code ${bashResult.code}`
66
- });
67
- } else {
68
- expansions.push({ command, output: trimmed });
69
- }
70
-
71
- result = result.replace(full, trimmed);
72
- } catch (err) {
73
- const errorMsg = err instanceof Error ? err.message : String(err);
74
- expansions.push({ command, output: '', error: errorMsg });
75
- result = result.replace(full, `[error: ${errorMsg}]`);
76
- }
77
- }
78
-
79
- // Show what was expanded (if UI available)
80
- if (ctx.hasUI && expansions.length > 0) {
81
- const summary = expansions
82
- .map((e) => {
83
- const status = e.error ? ` (${e.error})` : '';
84
- const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
85
- return `!{${e.command}}${status} -> "${preview}"`;
86
- })
87
- .join('\n');
88
-
89
- ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, 'info');
90
- }
91
-
92
- return { action: 'transform', text: result, images: event.images };
93
- });
94
- }
1
+ /**
2
+ * Inline Bash Extension - expands inline bash commands in user prompts.
3
+ *
4
+ * Start shortcut with this extension:
5
+ * shortcut -e ./examples/extensions/inline-bash.ts
6
+ *
7
+ * Then type prompts with inline bash:
8
+ * What's in !{pwd}?
9
+ * The current branch is !{git branch --show-current} and status: !{git status --short}
10
+ * My node version is !{node --version}
11
+ *
12
+ * The !{command} patterns are executed and replaced with their output before
13
+ * the prompt is sent to the agent.
14
+ *
15
+ * Note: Regular !command syntax (whole-line bash) is preserved and works as before.
16
+ */
17
+ import type { ExtensionAPI } from 'shortcutxl';
18
+
19
+ export default function (shortcut: ExtensionAPI) {
20
+ const PATTERN = /!\{([^}]+)\}/g;
21
+ const TIMEOUT_MS = 30000;
22
+
23
+ shortcut.on('input', async (event, ctx) => {
24
+ const text = event.text;
25
+
26
+ // Don't process if it's a whole-line bash command (starts with !)
27
+ // This preserves the existing !command behavior
28
+ if (text.trimStart().startsWith('!') && !text.trimStart().startsWith('!{')) {
29
+ return { action: 'continue' };
30
+ }
31
+
32
+ // Check if there are any inline bash patterns
33
+ if (!PATTERN.test(text)) {
34
+ return { action: 'continue' };
35
+ }
36
+
37
+ // Reset regex state after test()
38
+ PATTERN.lastIndex = 0;
39
+
40
+ let result = text;
41
+ const expansions: Array<{ command: string; output: string; error?: string }> = [];
42
+
43
+ // Find all matches first (to avoid issues with replacing while iterating)
44
+ const matches: Array<{ full: string; command: string }> = [];
45
+ let match = PATTERN.exec(text);
46
+ while (match) {
47
+ matches.push({ full: match[0], command: match[1] });
48
+ match = PATTERN.exec(text);
49
+ }
50
+
51
+ // Execute each command and collect results
52
+ for (const { full, command } of matches) {
53
+ try {
54
+ const bashResult = await shortcut.exec('bash', ['-c', command], {
55
+ timeout: TIMEOUT_MS
56
+ });
57
+
58
+ const output = bashResult.stdout || bashResult.stderr || '';
59
+ const trimmed = output.trim();
60
+
61
+ if (bashResult.code !== 0 && bashResult.stderr) {
62
+ expansions.push({
63
+ command,
64
+ output: trimmed,
65
+ error: `exit code ${bashResult.code}`
66
+ });
67
+ } else {
68
+ expansions.push({ command, output: trimmed });
69
+ }
70
+
71
+ result = result.replace(full, trimmed);
72
+ } catch (err) {
73
+ const errorMsg = err instanceof Error ? err.message : String(err);
74
+ expansions.push({ command, output: '', error: errorMsg });
75
+ result = result.replace(full, `[error: ${errorMsg}]`);
76
+ }
77
+ }
78
+
79
+ // Show what was expanded (if UI available)
80
+ if (ctx.hasUI && expansions.length > 0) {
81
+ const summary = expansions
82
+ .map((e) => {
83
+ const status = e.error ? ` (${e.error})` : '';
84
+ const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
85
+ return `!{${e.command}}${status} -> "${preview}"`;
86
+ })
87
+ .join('\n');
88
+
89
+ ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, 'info');
90
+ }
91
+
92
+ return { action: 'transform', text: result, images: event.images };
93
+ });
94
+ }
@@ -1,43 +1,43 @@
1
- /**
2
- * Input Transform Example - demonstrates the `input` event for intercepting user input.
3
- *
4
- * Start shortcut with this extension:
5
- * shortcut -e ./examples/extensions/input-transform.ts
6
- *
7
- * Then type these inside shortcut:
8
- * ?quick What is TypeScript? → "Respond briefly: What is TypeScript?"
9
- * ping → "pong" (instant, no LLM)
10
- * time → current time (instant, no LLM)
11
- */
12
- import type { ExtensionAPI } from 'shortcutxl';
13
-
14
- export default function (shortcut: ExtensionAPI) {
15
- shortcut.on('input', async (event, ctx) => {
16
- // Source-based logic: skip processing for extension-injected messages
17
- if (event.source === 'extension') {
18
- return { action: 'continue' };
19
- }
20
-
21
- // Transform: ?quick prefix for brief responses
22
- if (event.text.startsWith('?quick ')) {
23
- const query = event.text.slice(7).trim();
24
- if (!query) {
25
- ctx.ui.notify('Usage: ?quick <question>', 'warning');
26
- return { action: 'handled' };
27
- }
28
- return { action: 'transform', text: `Respond briefly in 1-2 sentences: ${query}` };
29
- }
30
-
31
- // Handle: instant responses without LLM (extension shows its own feedback)
32
- if (event.text.toLowerCase() === 'ping') {
33
- ctx.ui.notify('pong', 'info');
34
- return { action: 'handled' };
35
- }
36
- if (event.text.toLowerCase() === 'time') {
37
- ctx.ui.notify(new Date().toLocaleString(), 'info');
38
- return { action: 'handled' };
39
- }
40
-
41
- return { action: 'continue' };
42
- });
43
- }
1
+ /**
2
+ * Input Transform Example - demonstrates the `input` event for intercepting user input.
3
+ *
4
+ * Start shortcut with this extension:
5
+ * shortcut -e ./examples/extensions/input-transform.ts
6
+ *
7
+ * Then type these inside shortcut:
8
+ * ?quick What is TypeScript? → "Respond briefly: What is TypeScript?"
9
+ * ping → "pong" (instant, no LLM)
10
+ * time → current time (instant, no LLM)
11
+ */
12
+ import type { ExtensionAPI } from 'shortcutxl';
13
+
14
+ export default function (shortcut: ExtensionAPI) {
15
+ shortcut.on('input', async (event, ctx) => {
16
+ // Source-based logic: skip processing for extension-injected messages
17
+ if (event.source === 'extension') {
18
+ return { action: 'continue' };
19
+ }
20
+
21
+ // Transform: ?quick prefix for brief responses
22
+ if (event.text.startsWith('?quick ')) {
23
+ const query = event.text.slice(7).trim();
24
+ if (!query) {
25
+ ctx.ui.notify('Usage: ?quick <question>', 'warning');
26
+ return { action: 'handled' };
27
+ }
28
+ return { action: 'transform', text: `Respond briefly in 1-2 sentences: ${query}` };
29
+ }
30
+
31
+ // Handle: instant responses without LLM (extension shows its own feedback)
32
+ if (event.text.toLowerCase() === 'ping') {
33
+ ctx.ui.notify('pong', 'info');
34
+ return { action: 'handled' };
35
+ }
36
+ if (event.text.toLowerCase() === 'time') {
37
+ ctx.ui.notify(new Date().toLocaleString(), 'info');
38
+ return { action: 'handled' };
39
+ }
40
+
41
+ return { action: 'continue' };
42
+ });
43
+ }