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,566 +1,566 @@
1
- /**
2
- * Space Invaders game extension - play with /invaders command
3
- * Uses Kitty keyboard protocol for smooth movement (press/release detection)
4
- */
5
-
6
- import type { ExtensionAPI } from 'shortcutxl';
7
- import { isKeyRelease, Key, matchesKey, visibleWidth } from 'shortcutxl';
8
-
9
- const GAME_WIDTH = 60;
10
- const GAME_HEIGHT = 24;
11
- const TICK_MS = 50;
12
- const PLAYER_Y = GAME_HEIGHT - 2;
13
- const ALIEN_ROWS = 5;
14
- const ALIEN_COLS = 11;
15
- const ALIEN_START_Y = 2;
16
-
17
- type Point = { x: number; y: number };
18
-
19
- interface Bullet extends Point {
20
- direction: -1 | 1; // -1 = up (player), 1 = down (alien)
21
- }
22
-
23
- interface Alien extends Point {
24
- type: number; // 0, 1, 2 for different alien types
25
- alive: boolean;
26
- }
27
-
28
- interface Shield {
29
- x: number;
30
- segments: boolean[][]; // 4x3 grid of destructible segments
31
- }
32
-
33
- interface GameState {
34
- player: { x: number; lives: number };
35
- aliens: Alien[];
36
- alienDirection: 1 | -1;
37
- alienMoveCounter: number;
38
- alienMoveDelay: number;
39
- alienDropping: boolean;
40
- bullets: Bullet[];
41
- shields: Shield[];
42
- score: number;
43
- highScore: number;
44
- level: number;
45
- gameOver: boolean;
46
- victory: boolean;
47
- alienShootCounter: number;
48
- }
49
-
50
- interface KeyState {
51
- left: boolean;
52
- right: boolean;
53
- fire: boolean;
54
- }
55
-
56
- function createShields(): Shield[] {
57
- const shields: Shield[] = [];
58
- const shieldPositions = [8, 22, 36, 50];
59
- for (const x of shieldPositions) {
60
- shields.push({
61
- x,
62
- segments: [
63
- [true, true, true, true],
64
- [true, true, true, true],
65
- [true, false, false, true]
66
- ]
67
- });
68
- }
69
- return shields;
70
- }
71
-
72
- function createAliens(): Alien[] {
73
- const aliens: Alien[] = [];
74
- for (let row = 0; row < ALIEN_ROWS; row++) {
75
- const type = row === 0 ? 2 : row < 3 ? 1 : 0;
76
- for (let col = 0; col < ALIEN_COLS; col++) {
77
- aliens.push({
78
- x: 4 + col * 5,
79
- y: ALIEN_START_Y + row * 2,
80
- type,
81
- alive: true
82
- });
83
- }
84
- }
85
- return aliens;
86
- }
87
-
88
- function createInitialState(highScore = 0, level = 1): GameState {
89
- return {
90
- player: { x: Math.floor(GAME_WIDTH / 2), lives: 3 },
91
- aliens: createAliens(),
92
- alienDirection: 1,
93
- alienMoveCounter: 0,
94
- alienMoveDelay: Math.max(5, 20 - level * 2),
95
- alienDropping: false,
96
- bullets: [],
97
- shields: createShields(),
98
- score: 0,
99
- highScore,
100
- level,
101
- gameOver: false,
102
- victory: false,
103
- alienShootCounter: 0
104
- };
105
- }
106
-
107
- class SpaceInvadersComponent {
108
- private state: GameState;
109
- private keys: KeyState = { left: false, right: false, fire: false };
110
- private interval: ReturnType<typeof setInterval> | null = null;
111
- private onClose: () => void;
112
- private onSave: (state: GameState | null) => void;
113
- private tui: { requestRender: () => void };
114
- private cachedLines: string[] = [];
115
- private cachedWidth = 0;
116
- private version = 0;
117
- private cachedVersion = -1;
118
- private paused: boolean;
119
- private fireCooldown = 0;
120
- private playerMoveCounter = 0;
121
-
122
- // Opt-in to key release events for smooth movement
123
- wantsKeyRelease = true;
124
-
125
- constructor(
126
- tui: { requestRender: () => void },
127
- onClose: () => void,
128
- onSave: (state: GameState | null) => void,
129
- savedState?: GameState
130
- ) {
131
- this.tui = tui;
132
- if (savedState && !savedState.gameOver && !savedState.victory) {
133
- this.state = savedState;
134
- this.paused = true;
135
- } else {
136
- this.state = createInitialState(savedState?.highScore);
137
- this.paused = false;
138
- this.startGame();
139
- }
140
- this.onClose = onClose;
141
- this.onSave = onSave;
142
- }
143
-
144
- private startGame(): void {
145
- this.interval = setInterval(() => {
146
- if (!this.state.gameOver && !this.state.victory) {
147
- this.tick();
148
- this.version++;
149
- this.tui.requestRender();
150
- }
151
- }, TICK_MS);
152
- }
153
-
154
- private tick(): void {
155
- // Player movement (smooth, every other tick)
156
- this.playerMoveCounter++;
157
- if (this.playerMoveCounter >= 2) {
158
- this.playerMoveCounter = 0;
159
- if (this.keys.left && this.state.player.x > 2) {
160
- this.state.player.x--;
161
- }
162
- if (this.keys.right && this.state.player.x < GAME_WIDTH - 3) {
163
- this.state.player.x++;
164
- }
165
- }
166
-
167
- // Fire cooldown
168
- if (this.fireCooldown > 0) this.fireCooldown--;
169
-
170
- // Player shooting
171
- if (this.keys.fire && this.fireCooldown === 0) {
172
- const playerBullets = this.state.bullets.filter((b) => b.direction === -1);
173
- if (playerBullets.length < 2) {
174
- this.state.bullets.push({ x: this.state.player.x, y: PLAYER_Y - 1, direction: -1 });
175
- this.fireCooldown = 8;
176
- }
177
- }
178
-
179
- // Move bullets
180
- this.state.bullets = this.state.bullets.filter((bullet) => {
181
- bullet.y += bullet.direction;
182
- return bullet.y >= 0 && bullet.y < GAME_HEIGHT;
183
- });
184
-
185
- // Alien movement
186
- this.state.alienMoveCounter++;
187
- if (this.state.alienMoveCounter >= this.state.alienMoveDelay) {
188
- this.state.alienMoveCounter = 0;
189
- this.moveAliens();
190
- }
191
-
192
- // Alien shooting
193
- this.state.alienShootCounter++;
194
- if (this.state.alienShootCounter >= 30) {
195
- this.state.alienShootCounter = 0;
196
- this.alienShoot();
197
- }
198
-
199
- // Collision detection
200
- this.checkCollisions();
201
-
202
- // Check victory
203
- if (this.state.aliens.every((a) => !a.alive)) {
204
- this.state.victory = true;
205
- }
206
- }
207
-
208
- private moveAliens(): void {
209
- const aliveAliens = this.state.aliens.filter((a) => a.alive);
210
- if (aliveAliens.length === 0) return;
211
-
212
- if (this.state.alienDropping) {
213
- // Drop down
214
- for (const alien of aliveAliens) {
215
- alien.y++;
216
- if (alien.y >= PLAYER_Y - 1) {
217
- this.state.gameOver = true;
218
- return;
219
- }
220
- }
221
- this.state.alienDropping = false;
222
- } else {
223
- // Check if we need to change direction
224
- const minX = Math.min(...aliveAliens.map((a) => a.x));
225
- const maxX = Math.max(...aliveAliens.map((a) => a.x));
226
-
227
- if (
228
- (this.state.alienDirection === 1 && maxX >= GAME_WIDTH - 3) ||
229
- (this.state.alienDirection === -1 && minX <= 2)
230
- ) {
231
- this.state.alienDirection *= -1;
232
- this.state.alienDropping = true;
233
- } else {
234
- // Move horizontally
235
- for (const alien of aliveAliens) {
236
- alien.x += this.state.alienDirection;
237
- }
238
- }
239
- }
240
-
241
- // Speed up as fewer aliens remain
242
- const aliveCount = aliveAliens.length;
243
- if (aliveCount <= 5) {
244
- this.state.alienMoveDelay = 1;
245
- } else if (aliveCount <= 10) {
246
- this.state.alienMoveDelay = 2;
247
- } else if (aliveCount <= 20) {
248
- this.state.alienMoveDelay = 3;
249
- }
250
- }
251
-
252
- private alienShoot(): void {
253
- const aliveAliens = this.state.aliens.filter((a) => a.alive);
254
- if (aliveAliens.length === 0) return;
255
-
256
- // Find bottom-most alien in each column
257
- const columns = new Map<number, Alien>();
258
- for (const alien of aliveAliens) {
259
- const existing = columns.get(alien.x);
260
- if (!existing || alien.y > existing.y) {
261
- columns.set(alien.x, alien);
262
- }
263
- }
264
-
265
- // Random column shoots
266
- const shooters = Array.from(columns.values());
267
- if (shooters.length > 0 && this.state.bullets.filter((b) => b.direction === 1).length < 3) {
268
- const shooter = shooters[Math.floor(Math.random() * shooters.length)];
269
- this.state.bullets.push({ x: shooter.x, y: shooter.y + 1, direction: 1 });
270
- }
271
- }
272
-
273
- private checkCollisions(): void {
274
- const bulletsToRemove = new Set<Bullet>();
275
-
276
- for (const bullet of this.state.bullets) {
277
- // Player bullets hitting aliens
278
- if (bullet.direction === -1) {
279
- for (const alien of this.state.aliens) {
280
- if (alien.alive && Math.abs(bullet.x - alien.x) <= 1 && bullet.y === alien.y) {
281
- alien.alive = false;
282
- bulletsToRemove.add(bullet);
283
- const points = [10, 20, 30][alien.type];
284
- this.state.score += points;
285
- if (this.state.score > this.state.highScore) {
286
- this.state.highScore = this.state.score;
287
- }
288
- break;
289
- }
290
- }
291
- }
292
-
293
- // Alien bullets hitting player
294
- if (bullet.direction === 1) {
295
- if (Math.abs(bullet.x - this.state.player.x) <= 1 && bullet.y === PLAYER_Y) {
296
- bulletsToRemove.add(bullet);
297
- this.state.player.lives--;
298
- if (this.state.player.lives <= 0) {
299
- this.state.gameOver = true;
300
- }
301
- }
302
- }
303
-
304
- // Bullets hitting shields
305
- for (const shield of this.state.shields) {
306
- const relX = bullet.x - shield.x;
307
- const relY = bullet.y - (PLAYER_Y - 5);
308
- if (relX >= 0 && relX < 4 && relY >= 0 && relY < 3) {
309
- if (shield.segments[relY][relX]) {
310
- shield.segments[relY][relX] = false;
311
- bulletsToRemove.add(bullet);
312
- }
313
- }
314
- }
315
- }
316
-
317
- this.state.bullets = this.state.bullets.filter((b) => !bulletsToRemove.has(b));
318
- }
319
-
320
- handleInput(data: string): void {
321
- const released = isKeyRelease(data);
322
-
323
- // Pause handling
324
- if (this.paused && !released) {
325
- if (matchesKey(data, Key.escape) || data === 'q' || data === 'Q') {
326
- this.dispose();
327
- this.onClose();
328
- return;
329
- }
330
- this.paused = false;
331
- this.startGame();
332
- return;
333
- }
334
-
335
- // ESC to pause and save
336
- if (!released && matchesKey(data, Key.escape)) {
337
- this.dispose();
338
- this.onSave(this.state);
339
- this.onClose();
340
- return;
341
- }
342
-
343
- // Q to quit without saving
344
- if (!released && (data === 'q' || data === 'Q')) {
345
- this.dispose();
346
- this.onSave(null);
347
- this.onClose();
348
- return;
349
- }
350
-
351
- // Movement keys (track press/release state)
352
- if (matchesKey(data, Key.left) || data === 'a' || data === 'A' || matchesKey(data, 'a')) {
353
- this.keys.left = !released;
354
- }
355
- if (matchesKey(data, Key.right) || data === 'd' || data === 'D' || matchesKey(data, 'd')) {
356
- this.keys.right = !released;
357
- }
358
-
359
- // Fire key
360
- if (
361
- matchesKey(data, Key.space) ||
362
- data === ' ' ||
363
- data === 'f' ||
364
- data === 'F' ||
365
- matchesKey(data, 'f')
366
- ) {
367
- this.keys.fire = !released;
368
- }
369
-
370
- // Restart on game over or victory
371
- if (!released && (this.state.gameOver || this.state.victory)) {
372
- if (data === 'r' || data === 'R' || data === ' ') {
373
- const highScore = this.state.highScore;
374
- const nextLevel = this.state.victory ? this.state.level + 1 : 1;
375
- this.state = createInitialState(highScore, nextLevel);
376
- this.keys = { left: false, right: false, fire: false };
377
- this.onSave(null);
378
- this.version++;
379
- this.tui.requestRender();
380
- }
381
- }
382
- }
383
-
384
- invalidate(): void {
385
- this.cachedWidth = 0;
386
- }
387
-
388
- render(width: number): string[] {
389
- if (width === this.cachedWidth && this.cachedVersion === this.version) {
390
- return this.cachedLines;
391
- }
392
-
393
- const lines: string[] = [];
394
-
395
- // Colors
396
- const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
397
- const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
398
- const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
399
- const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
400
- const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
401
- const magenta = (s: string) => `\x1b[35m${s}\x1b[0m`;
402
- const white = (s: string) => `\x1b[97m${s}\x1b[0m`;
403
- const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
404
-
405
- const boxWidth = GAME_WIDTH;
406
-
407
- const boxLine = (content: string) => {
408
- const contentLen = visibleWidth(content);
409
- const padding = Math.max(0, boxWidth - contentLen);
410
- return dim(' │') + content + ' '.repeat(padding) + dim('│');
411
- };
412
-
413
- // Top border
414
- lines.push(this.padLine(dim(` ╭${'─'.repeat(boxWidth)}╮`), width));
415
-
416
- // Header
417
- const title = `${bold(green('SPACE INVADERS'))}`;
418
- const scoreText = `Score: ${bold(yellow(String(this.state.score)))}`;
419
- const highText = `Hi: ${bold(yellow(String(this.state.highScore)))}`;
420
- const levelText = `Lv: ${bold(cyan(String(this.state.level)))}`;
421
- const livesText = `${red('♥'.repeat(this.state.player.lives))}`;
422
- const header = `${title} │ ${scoreText} │ ${highText} │ ${levelText} │ ${livesText}`;
423
- lines.push(this.padLine(boxLine(header), width));
424
-
425
- // Separator
426
- lines.push(this.padLine(dim(` ├${'─'.repeat(boxWidth)}┤`), width));
427
-
428
- // Game grid
429
- for (let y = 0; y < GAME_HEIGHT; y++) {
430
- let row = '';
431
- for (let x = 0; x < GAME_WIDTH; x++) {
432
- let char = ' ';
433
- let colored = false;
434
-
435
- // Check aliens
436
- for (const alien of this.state.aliens) {
437
- if (alien.alive && alien.y === y && Math.abs(alien.x - x) <= 1) {
438
- const sprites = [
439
- x === alien.x ? '▼' : '╲╱'[x < alien.x ? 0 : 1],
440
- x === alien.x ? '◆' : '╱╲'[x < alien.x ? 0 : 1],
441
- x === alien.x ? '☆' : '◄►'[x < alien.x ? 0 : 1]
442
- ];
443
- const colors = [green, cyan, magenta];
444
- char = colors[alien.type](sprites[alien.type]);
445
- colored = true;
446
- break;
447
- }
448
- }
449
-
450
- // Check shields
451
- if (!colored) {
452
- for (const shield of this.state.shields) {
453
- const relX = x - shield.x;
454
- const relY = y - (PLAYER_Y - 5);
455
- if (relX >= 0 && relX < 4 && relY >= 0 && relY < 3) {
456
- if (shield.segments[relY][relX]) {
457
- char = dim('█');
458
- colored = true;
459
- }
460
- break;
461
- }
462
- }
463
- }
464
-
465
- // Check player
466
- if (!colored && y === PLAYER_Y && Math.abs(x - this.state.player.x) <= 1) {
467
- if (x === this.state.player.x) {
468
- char = white('▲');
469
- } else {
470
- char = white('═');
471
- }
472
- colored = true;
473
- }
474
-
475
- // Check bullets
476
- if (!colored) {
477
- for (const bullet of this.state.bullets) {
478
- if (bullet.x === x && bullet.y === y) {
479
- char = bullet.direction === -1 ? yellow('│') : red('│');
480
- colored = true;
481
- break;
482
- }
483
- }
484
- }
485
-
486
- row += colored ? char : ' ';
487
- }
488
- lines.push(this.padLine(dim(' │') + row + dim('│'), width));
489
- }
490
-
491
- // Separator
492
- lines.push(this.padLine(dim(` ├${'─'.repeat(boxWidth)}┤`), width));
493
-
494
- // Footer
495
- let footer: string;
496
- if (this.paused) {
497
- footer = `${yellow(bold('PAUSED'))} Press any key to continue, ${bold('Q')} to quit`;
498
- } else if (this.state.gameOver) {
499
- footer = `${red(bold('GAME OVER!'))} Press ${bold('R')} to restart, ${bold('Q')} to quit`;
500
- } else if (this.state.victory) {
501
- footer = `${green(bold('VICTORY!'))} Press ${bold('R')} for level ${this.state.level + 1}, ${bold('Q')} to quit`;
502
- } else {
503
- footer = `←→ or AD to move, ${bold('SPACE')}/F to fire, ${bold('ESC')} pause, ${bold('Q')} quit`;
504
- }
505
- lines.push(this.padLine(boxLine(footer), width));
506
-
507
- // Bottom border
508
- lines.push(this.padLine(dim(` ╰${'─'.repeat(boxWidth)}╯`), width));
509
-
510
- this.cachedLines = lines;
511
- this.cachedWidth = width;
512
- this.cachedVersion = this.version;
513
-
514
- return lines;
515
- }
516
-
517
- private padLine(line: string, width: number): string {
518
- const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
519
- const padding = Math.max(0, width - visibleLen);
520
- return line + ' '.repeat(padding);
521
- }
522
-
523
- dispose(): void {
524
- if (this.interval) {
525
- clearInterval(this.interval);
526
- this.interval = null;
527
- }
528
- }
529
- }
530
-
531
- const INVADERS_SAVE_TYPE = 'space-invaders-save';
532
-
533
- export default function (shortcut: ExtensionAPI) {
534
- shortcut.registerCommand('invaders', {
535
- description: 'Play Space Invaders!',
536
-
537
- handler: async (_args, ctx) => {
538
- if (!ctx.hasUI) {
539
- ctx.ui.notify('Space Invaders requires interactive mode', 'error');
540
- return;
541
- }
542
-
543
- // Load saved state from session
544
- const entries = ctx.sessionManager.getEntries();
545
- let savedState: GameState | undefined;
546
- for (let i = entries.length - 1; i >= 0; i--) {
547
- const entry = entries[i];
548
- if (entry.type === 'custom' && entry.customType === INVADERS_SAVE_TYPE) {
549
- savedState = entry.data as GameState;
550
- break;
551
- }
552
- }
553
-
554
- await ctx.ui.custom((tui, _theme, _kb, done) => {
555
- return new SpaceInvadersComponent(
556
- tui,
557
- () => done(undefined),
558
- (state) => {
559
- shortcut.appendEntry(INVADERS_SAVE_TYPE, state);
560
- },
561
- savedState
562
- );
563
- });
564
- }
565
- });
566
- }
1
+ /**
2
+ * Space Invaders game extension - play with /invaders command
3
+ * Uses Kitty keyboard protocol for smooth movement (press/release detection)
4
+ */
5
+
6
+ import type { ExtensionAPI } from 'shortcutxl';
7
+ import { isKeyRelease, Key, matchesKey, visibleWidth } from 'shortcutxl';
8
+
9
+ const GAME_WIDTH = 60;
10
+ const GAME_HEIGHT = 24;
11
+ const TICK_MS = 50;
12
+ const PLAYER_Y = GAME_HEIGHT - 2;
13
+ const ALIEN_ROWS = 5;
14
+ const ALIEN_COLS = 11;
15
+ const ALIEN_START_Y = 2;
16
+
17
+ type Point = { x: number; y: number };
18
+
19
+ interface Bullet extends Point {
20
+ direction: -1 | 1; // -1 = up (player), 1 = down (alien)
21
+ }
22
+
23
+ interface Alien extends Point {
24
+ type: number; // 0, 1, 2 for different alien types
25
+ alive: boolean;
26
+ }
27
+
28
+ interface Shield {
29
+ x: number;
30
+ segments: boolean[][]; // 4x3 grid of destructible segments
31
+ }
32
+
33
+ interface GameState {
34
+ player: { x: number; lives: number };
35
+ aliens: Alien[];
36
+ alienDirection: 1 | -1;
37
+ alienMoveCounter: number;
38
+ alienMoveDelay: number;
39
+ alienDropping: boolean;
40
+ bullets: Bullet[];
41
+ shields: Shield[];
42
+ score: number;
43
+ highScore: number;
44
+ level: number;
45
+ gameOver: boolean;
46
+ victory: boolean;
47
+ alienShootCounter: number;
48
+ }
49
+
50
+ interface KeyState {
51
+ left: boolean;
52
+ right: boolean;
53
+ fire: boolean;
54
+ }
55
+
56
+ function createShields(): Shield[] {
57
+ const shields: Shield[] = [];
58
+ const shieldPositions = [8, 22, 36, 50];
59
+ for (const x of shieldPositions) {
60
+ shields.push({
61
+ x,
62
+ segments: [
63
+ [true, true, true, true],
64
+ [true, true, true, true],
65
+ [true, false, false, true]
66
+ ]
67
+ });
68
+ }
69
+ return shields;
70
+ }
71
+
72
+ function createAliens(): Alien[] {
73
+ const aliens: Alien[] = [];
74
+ for (let row = 0; row < ALIEN_ROWS; row++) {
75
+ const type = row === 0 ? 2 : row < 3 ? 1 : 0;
76
+ for (let col = 0; col < ALIEN_COLS; col++) {
77
+ aliens.push({
78
+ x: 4 + col * 5,
79
+ y: ALIEN_START_Y + row * 2,
80
+ type,
81
+ alive: true
82
+ });
83
+ }
84
+ }
85
+ return aliens;
86
+ }
87
+
88
+ function createInitialState(highScore = 0, level = 1): GameState {
89
+ return {
90
+ player: { x: Math.floor(GAME_WIDTH / 2), lives: 3 },
91
+ aliens: createAliens(),
92
+ alienDirection: 1,
93
+ alienMoveCounter: 0,
94
+ alienMoveDelay: Math.max(5, 20 - level * 2),
95
+ alienDropping: false,
96
+ bullets: [],
97
+ shields: createShields(),
98
+ score: 0,
99
+ highScore,
100
+ level,
101
+ gameOver: false,
102
+ victory: false,
103
+ alienShootCounter: 0
104
+ };
105
+ }
106
+
107
+ class SpaceInvadersComponent {
108
+ private state: GameState;
109
+ private keys: KeyState = { left: false, right: false, fire: false };
110
+ private interval: ReturnType<typeof setInterval> | null = null;
111
+ private onClose: () => void;
112
+ private onSave: (state: GameState | null) => void;
113
+ private tui: { requestRender: () => void };
114
+ private cachedLines: string[] = [];
115
+ private cachedWidth = 0;
116
+ private version = 0;
117
+ private cachedVersion = -1;
118
+ private paused: boolean;
119
+ private fireCooldown = 0;
120
+ private playerMoveCounter = 0;
121
+
122
+ // Opt-in to key release events for smooth movement
123
+ wantsKeyRelease = true;
124
+
125
+ constructor(
126
+ tui: { requestRender: () => void },
127
+ onClose: () => void,
128
+ onSave: (state: GameState | null) => void,
129
+ savedState?: GameState
130
+ ) {
131
+ this.tui = tui;
132
+ if (savedState && !savedState.gameOver && !savedState.victory) {
133
+ this.state = savedState;
134
+ this.paused = true;
135
+ } else {
136
+ this.state = createInitialState(savedState?.highScore);
137
+ this.paused = false;
138
+ this.startGame();
139
+ }
140
+ this.onClose = onClose;
141
+ this.onSave = onSave;
142
+ }
143
+
144
+ private startGame(): void {
145
+ this.interval = setInterval(() => {
146
+ if (!this.state.gameOver && !this.state.victory) {
147
+ this.tick();
148
+ this.version++;
149
+ this.tui.requestRender();
150
+ }
151
+ }, TICK_MS);
152
+ }
153
+
154
+ private tick(): void {
155
+ // Player movement (smooth, every other tick)
156
+ this.playerMoveCounter++;
157
+ if (this.playerMoveCounter >= 2) {
158
+ this.playerMoveCounter = 0;
159
+ if (this.keys.left && this.state.player.x > 2) {
160
+ this.state.player.x--;
161
+ }
162
+ if (this.keys.right && this.state.player.x < GAME_WIDTH - 3) {
163
+ this.state.player.x++;
164
+ }
165
+ }
166
+
167
+ // Fire cooldown
168
+ if (this.fireCooldown > 0) this.fireCooldown--;
169
+
170
+ // Player shooting
171
+ if (this.keys.fire && this.fireCooldown === 0) {
172
+ const playerBullets = this.state.bullets.filter((b) => b.direction === -1);
173
+ if (playerBullets.length < 2) {
174
+ this.state.bullets.push({ x: this.state.player.x, y: PLAYER_Y - 1, direction: -1 });
175
+ this.fireCooldown = 8;
176
+ }
177
+ }
178
+
179
+ // Move bullets
180
+ this.state.bullets = this.state.bullets.filter((bullet) => {
181
+ bullet.y += bullet.direction;
182
+ return bullet.y >= 0 && bullet.y < GAME_HEIGHT;
183
+ });
184
+
185
+ // Alien movement
186
+ this.state.alienMoveCounter++;
187
+ if (this.state.alienMoveCounter >= this.state.alienMoveDelay) {
188
+ this.state.alienMoveCounter = 0;
189
+ this.moveAliens();
190
+ }
191
+
192
+ // Alien shooting
193
+ this.state.alienShootCounter++;
194
+ if (this.state.alienShootCounter >= 30) {
195
+ this.state.alienShootCounter = 0;
196
+ this.alienShoot();
197
+ }
198
+
199
+ // Collision detection
200
+ this.checkCollisions();
201
+
202
+ // Check victory
203
+ if (this.state.aliens.every((a) => !a.alive)) {
204
+ this.state.victory = true;
205
+ }
206
+ }
207
+
208
+ private moveAliens(): void {
209
+ const aliveAliens = this.state.aliens.filter((a) => a.alive);
210
+ if (aliveAliens.length === 0) return;
211
+
212
+ if (this.state.alienDropping) {
213
+ // Drop down
214
+ for (const alien of aliveAliens) {
215
+ alien.y++;
216
+ if (alien.y >= PLAYER_Y - 1) {
217
+ this.state.gameOver = true;
218
+ return;
219
+ }
220
+ }
221
+ this.state.alienDropping = false;
222
+ } else {
223
+ // Check if we need to change direction
224
+ const minX = Math.min(...aliveAliens.map((a) => a.x));
225
+ const maxX = Math.max(...aliveAliens.map((a) => a.x));
226
+
227
+ if (
228
+ (this.state.alienDirection === 1 && maxX >= GAME_WIDTH - 3) ||
229
+ (this.state.alienDirection === -1 && minX <= 2)
230
+ ) {
231
+ this.state.alienDirection *= -1;
232
+ this.state.alienDropping = true;
233
+ } else {
234
+ // Move horizontally
235
+ for (const alien of aliveAliens) {
236
+ alien.x += this.state.alienDirection;
237
+ }
238
+ }
239
+ }
240
+
241
+ // Speed up as fewer aliens remain
242
+ const aliveCount = aliveAliens.length;
243
+ if (aliveCount <= 5) {
244
+ this.state.alienMoveDelay = 1;
245
+ } else if (aliveCount <= 10) {
246
+ this.state.alienMoveDelay = 2;
247
+ } else if (aliveCount <= 20) {
248
+ this.state.alienMoveDelay = 3;
249
+ }
250
+ }
251
+
252
+ private alienShoot(): void {
253
+ const aliveAliens = this.state.aliens.filter((a) => a.alive);
254
+ if (aliveAliens.length === 0) return;
255
+
256
+ // Find bottom-most alien in each column
257
+ const columns = new Map<number, Alien>();
258
+ for (const alien of aliveAliens) {
259
+ const existing = columns.get(alien.x);
260
+ if (!existing || alien.y > existing.y) {
261
+ columns.set(alien.x, alien);
262
+ }
263
+ }
264
+
265
+ // Random column shoots
266
+ const shooters = Array.from(columns.values());
267
+ if (shooters.length > 0 && this.state.bullets.filter((b) => b.direction === 1).length < 3) {
268
+ const shooter = shooters[Math.floor(Math.random() * shooters.length)];
269
+ this.state.bullets.push({ x: shooter.x, y: shooter.y + 1, direction: 1 });
270
+ }
271
+ }
272
+
273
+ private checkCollisions(): void {
274
+ const bulletsToRemove = new Set<Bullet>();
275
+
276
+ for (const bullet of this.state.bullets) {
277
+ // Player bullets hitting aliens
278
+ if (bullet.direction === -1) {
279
+ for (const alien of this.state.aliens) {
280
+ if (alien.alive && Math.abs(bullet.x - alien.x) <= 1 && bullet.y === alien.y) {
281
+ alien.alive = false;
282
+ bulletsToRemove.add(bullet);
283
+ const points = [10, 20, 30][alien.type];
284
+ this.state.score += points;
285
+ if (this.state.score > this.state.highScore) {
286
+ this.state.highScore = this.state.score;
287
+ }
288
+ break;
289
+ }
290
+ }
291
+ }
292
+
293
+ // Alien bullets hitting player
294
+ if (bullet.direction === 1) {
295
+ if (Math.abs(bullet.x - this.state.player.x) <= 1 && bullet.y === PLAYER_Y) {
296
+ bulletsToRemove.add(bullet);
297
+ this.state.player.lives--;
298
+ if (this.state.player.lives <= 0) {
299
+ this.state.gameOver = true;
300
+ }
301
+ }
302
+ }
303
+
304
+ // Bullets hitting shields
305
+ for (const shield of this.state.shields) {
306
+ const relX = bullet.x - shield.x;
307
+ const relY = bullet.y - (PLAYER_Y - 5);
308
+ if (relX >= 0 && relX < 4 && relY >= 0 && relY < 3) {
309
+ if (shield.segments[relY][relX]) {
310
+ shield.segments[relY][relX] = false;
311
+ bulletsToRemove.add(bullet);
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ this.state.bullets = this.state.bullets.filter((b) => !bulletsToRemove.has(b));
318
+ }
319
+
320
+ handleInput(data: string): void {
321
+ const released = isKeyRelease(data);
322
+
323
+ // Pause handling
324
+ if (this.paused && !released) {
325
+ if (matchesKey(data, Key.escape) || data === 'q' || data === 'Q') {
326
+ this.dispose();
327
+ this.onClose();
328
+ return;
329
+ }
330
+ this.paused = false;
331
+ this.startGame();
332
+ return;
333
+ }
334
+
335
+ // ESC to pause and save
336
+ if (!released && matchesKey(data, Key.escape)) {
337
+ this.dispose();
338
+ this.onSave(this.state);
339
+ this.onClose();
340
+ return;
341
+ }
342
+
343
+ // Q to quit without saving
344
+ if (!released && (data === 'q' || data === 'Q')) {
345
+ this.dispose();
346
+ this.onSave(null);
347
+ this.onClose();
348
+ return;
349
+ }
350
+
351
+ // Movement keys (track press/release state)
352
+ if (matchesKey(data, Key.left) || data === 'a' || data === 'A' || matchesKey(data, 'a')) {
353
+ this.keys.left = !released;
354
+ }
355
+ if (matchesKey(data, Key.right) || data === 'd' || data === 'D' || matchesKey(data, 'd')) {
356
+ this.keys.right = !released;
357
+ }
358
+
359
+ // Fire key
360
+ if (
361
+ matchesKey(data, Key.space) ||
362
+ data === ' ' ||
363
+ data === 'f' ||
364
+ data === 'F' ||
365
+ matchesKey(data, 'f')
366
+ ) {
367
+ this.keys.fire = !released;
368
+ }
369
+
370
+ // Restart on game over or victory
371
+ if (!released && (this.state.gameOver || this.state.victory)) {
372
+ if (data === 'r' || data === 'R' || data === ' ') {
373
+ const highScore = this.state.highScore;
374
+ const nextLevel = this.state.victory ? this.state.level + 1 : 1;
375
+ this.state = createInitialState(highScore, nextLevel);
376
+ this.keys = { left: false, right: false, fire: false };
377
+ this.onSave(null);
378
+ this.version++;
379
+ this.tui.requestRender();
380
+ }
381
+ }
382
+ }
383
+
384
+ invalidate(): void {
385
+ this.cachedWidth = 0;
386
+ }
387
+
388
+ render(width: number): string[] {
389
+ if (width === this.cachedWidth && this.cachedVersion === this.version) {
390
+ return this.cachedLines;
391
+ }
392
+
393
+ const lines: string[] = [];
394
+
395
+ // Colors
396
+ const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
397
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
398
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
399
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
400
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
401
+ const magenta = (s: string) => `\x1b[35m${s}\x1b[0m`;
402
+ const white = (s: string) => `\x1b[97m${s}\x1b[0m`;
403
+ const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
404
+
405
+ const boxWidth = GAME_WIDTH;
406
+
407
+ const boxLine = (content: string) => {
408
+ const contentLen = visibleWidth(content);
409
+ const padding = Math.max(0, boxWidth - contentLen);
410
+ return dim(' │') + content + ' '.repeat(padding) + dim('│');
411
+ };
412
+
413
+ // Top border
414
+ lines.push(this.padLine(dim(` ╭${'─'.repeat(boxWidth)}╮`), width));
415
+
416
+ // Header
417
+ const title = `${bold(green('SPACE INVADERS'))}`;
418
+ const scoreText = `Score: ${bold(yellow(String(this.state.score)))}`;
419
+ const highText = `Hi: ${bold(yellow(String(this.state.highScore)))}`;
420
+ const levelText = `Lv: ${bold(cyan(String(this.state.level)))}`;
421
+ const livesText = `${red('♥'.repeat(this.state.player.lives))}`;
422
+ const header = `${title} │ ${scoreText} │ ${highText} │ ${levelText} │ ${livesText}`;
423
+ lines.push(this.padLine(boxLine(header), width));
424
+
425
+ // Separator
426
+ lines.push(this.padLine(dim(` ├${'─'.repeat(boxWidth)}┤`), width));
427
+
428
+ // Game grid
429
+ for (let y = 0; y < GAME_HEIGHT; y++) {
430
+ let row = '';
431
+ for (let x = 0; x < GAME_WIDTH; x++) {
432
+ let char = ' ';
433
+ let colored = false;
434
+
435
+ // Check aliens
436
+ for (const alien of this.state.aliens) {
437
+ if (alien.alive && alien.y === y && Math.abs(alien.x - x) <= 1) {
438
+ const sprites = [
439
+ x === alien.x ? '▼' : '╲╱'[x < alien.x ? 0 : 1],
440
+ x === alien.x ? '◆' : '╱╲'[x < alien.x ? 0 : 1],
441
+ x === alien.x ? '☆' : '◄►'[x < alien.x ? 0 : 1]
442
+ ];
443
+ const colors = [green, cyan, magenta];
444
+ char = colors[alien.type](sprites[alien.type]);
445
+ colored = true;
446
+ break;
447
+ }
448
+ }
449
+
450
+ // Check shields
451
+ if (!colored) {
452
+ for (const shield of this.state.shields) {
453
+ const relX = x - shield.x;
454
+ const relY = y - (PLAYER_Y - 5);
455
+ if (relX >= 0 && relX < 4 && relY >= 0 && relY < 3) {
456
+ if (shield.segments[relY][relX]) {
457
+ char = dim('█');
458
+ colored = true;
459
+ }
460
+ break;
461
+ }
462
+ }
463
+ }
464
+
465
+ // Check player
466
+ if (!colored && y === PLAYER_Y && Math.abs(x - this.state.player.x) <= 1) {
467
+ if (x === this.state.player.x) {
468
+ char = white('▲');
469
+ } else {
470
+ char = white('═');
471
+ }
472
+ colored = true;
473
+ }
474
+
475
+ // Check bullets
476
+ if (!colored) {
477
+ for (const bullet of this.state.bullets) {
478
+ if (bullet.x === x && bullet.y === y) {
479
+ char = bullet.direction === -1 ? yellow('│') : red('│');
480
+ colored = true;
481
+ break;
482
+ }
483
+ }
484
+ }
485
+
486
+ row += colored ? char : ' ';
487
+ }
488
+ lines.push(this.padLine(dim(' │') + row + dim('│'), width));
489
+ }
490
+
491
+ // Separator
492
+ lines.push(this.padLine(dim(` ├${'─'.repeat(boxWidth)}┤`), width));
493
+
494
+ // Footer
495
+ let footer: string;
496
+ if (this.paused) {
497
+ footer = `${yellow(bold('PAUSED'))} Press any key to continue, ${bold('Q')} to quit`;
498
+ } else if (this.state.gameOver) {
499
+ footer = `${red(bold('GAME OVER!'))} Press ${bold('R')} to restart, ${bold('Q')} to quit`;
500
+ } else if (this.state.victory) {
501
+ footer = `${green(bold('VICTORY!'))} Press ${bold('R')} for level ${this.state.level + 1}, ${bold('Q')} to quit`;
502
+ } else {
503
+ footer = `←→ or AD to move, ${bold('SPACE')}/F to fire, ${bold('ESC')} pause, ${bold('Q')} quit`;
504
+ }
505
+ lines.push(this.padLine(boxLine(footer), width));
506
+
507
+ // Bottom border
508
+ lines.push(this.padLine(dim(` ╰${'─'.repeat(boxWidth)}╯`), width));
509
+
510
+ this.cachedLines = lines;
511
+ this.cachedWidth = width;
512
+ this.cachedVersion = this.version;
513
+
514
+ return lines;
515
+ }
516
+
517
+ private padLine(line: string, width: number): string {
518
+ const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
519
+ const padding = Math.max(0, width - visibleLen);
520
+ return line + ' '.repeat(padding);
521
+ }
522
+
523
+ dispose(): void {
524
+ if (this.interval) {
525
+ clearInterval(this.interval);
526
+ this.interval = null;
527
+ }
528
+ }
529
+ }
530
+
531
+ const INVADERS_SAVE_TYPE = 'space-invaders-save';
532
+
533
+ export default function (shortcut: ExtensionAPI) {
534
+ shortcut.registerCommand('invaders', {
535
+ description: 'Play Space Invaders!',
536
+
537
+ handler: async (_args, ctx) => {
538
+ if (!ctx.hasUI) {
539
+ ctx.ui.notify('Space Invaders requires interactive mode', 'error');
540
+ return;
541
+ }
542
+
543
+ // Load saved state from session
544
+ const entries = ctx.sessionManager.getEntries();
545
+ let savedState: GameState | undefined;
546
+ for (let i = entries.length - 1; i >= 0; i--) {
547
+ const entry = entries[i];
548
+ if (entry.type === 'custom' && entry.customType === INVADERS_SAVE_TYPE) {
549
+ savedState = entry.data as GameState;
550
+ break;
551
+ }
552
+ }
553
+
554
+ await ctx.ui.custom((tui, _theme, _kb, done) => {
555
+ return new SpaceInvadersComponent(
556
+ tui,
557
+ () => done(undefined),
558
+ (state) => {
559
+ shortcut.appendEntry(INVADERS_SAVE_TYPE, state);
560
+ },
561
+ savedState
562
+ );
563
+ });
564
+ }
565
+ });
566
+ }