@boyingliu01/xp-gate 0.25.0 → 0.27.0

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 (120) hide show
  1. package/adapters/python.sh +53 -3
  2. package/bin/xp-gate.js +161 -28
  3. package/hooks/lib/python-incremental-typecheck.sh +104 -0
  4. package/hooks/lib/typecheck.sh +10 -0
  5. package/hooks/pre-commit +84 -32
  6. package/lib/__tests__/check-alignment-cli.test.mjs +77 -0
  7. package/lib/__tests__/doctor-469-hook-env.test.js +12 -7
  8. package/lib/__tests__/doctor-module-resolution-parity.test.js +15 -6
  9. package/lib/__tests__/doctor-tui-skill-freshness.test.js +16 -4
  10. package/lib/__tests__/test-alignment.test.ts +37 -0
  11. package/lib/__tests__/windows-bash-probe.test.js +73 -0
  12. package/lib/check-alignment-runner.cjs +74 -0
  13. package/lib/detect-deps.js +82 -19
  14. package/lib/gate-runner.js +12 -5
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/lib/sprint-status.js +5 -2
  17. package/lib/test-alignment.ts +136 -12
  18. package/mock-policy/AGENTS.md +3 -3
  19. package/mutation/AGENTS.md +3 -3
  20. package/package.json +1 -1
  21. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  22. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  23. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  24. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  25. package/plugins/dsh/README.md +1 -1
  26. package/plugins/dsh/package.json +1 -1
  27. package/plugins/dsh/scripts/prepack.cjs +2 -2
  28. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  29. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/dsh/src/skills.test.ts +20 -7
  32. package/plugins/opencode/package.json +1 -1
  33. package/plugins/opencode/scripts/prepack.cjs +0 -1
  34. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  35. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  36. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  37. package/plugins/qoder/plugin.json +1 -1
  38. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  41. package/principles/AGENTS.md +3 -3
  42. package/principles/rules/__tests__/clean-code/code-duplication.test.ts +9 -8
  43. package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +3 -4
  44. package/principles/rules/__tests__/clean-code/god-class.test.ts +2 -4
  45. package/principles/rules/__tests__/clean-code/large-file.test.ts +2 -4
  46. package/principles/rules/__tests__/clean-code/long-function.test.ts +2 -4
  47. package/principles/rules/__tests__/clean-code/many-exports.test.ts +2 -3
  48. package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +2 -4
  49. package/principles/rules/__tests__/clean-code/too-many-params.test.ts +2 -4
  50. package/principles/rules/__tests__/clean-code/unused-imports.test.ts +4 -4
  51. package/principles/rules/__tests__/solid/dip.test.ts +2 -4
  52. package/principles/rules/__tests__/solid/isp.test.ts +2 -4
  53. package/principles/rules/__tests__/solid/lsp.test.ts +2 -4
  54. package/principles/rules/__tests__/solid/ocp.test.ts +2 -4
  55. package/principles/rules/__tests__/solid/srp.test.ts +2 -4
  56. package/principles/rules/clean-code/code-duplication.ts +12 -14
  57. package/principles/rules/clean-code/deep-nesting.ts +18 -20
  58. package/principles/rules/clean-code/god-class.ts +32 -34
  59. package/principles/rules/clean-code/large-file.ts +14 -16
  60. package/principles/rules/clean-code/long-function.ts +26 -28
  61. package/principles/rules/clean-code/magic-numbers.ts +39 -38
  62. package/principles/rules/clean-code/many-exports.ts +21 -23
  63. package/principles/rules/clean-code/missing-error-handling.ts +13 -15
  64. package/principles/rules/clean-code/too-many-params.ts +15 -17
  65. package/principles/rules/clean-code/unused-imports.ts +13 -15
  66. package/principles/rules/solid/dip.ts +16 -18
  67. package/principles/rules/solid/isp.ts +17 -19
  68. package/principles/rules/solid/lsp.ts +4 -6
  69. package/principles/rules/solid/ocp.ts +17 -19
  70. package/principles/rules/solid/srp.ts +20 -22
  71. package/scripts/__tests__/sync-package-content.test.js +0 -1
  72. package/scripts/sync-package-content.js +0 -1
  73. package/skills/delphi-review/AGENTS.md +3 -3
  74. package/skills/sprint-flow/AGENTS.md +3 -3
  75. package/skills/test-specification-alignment/AGENTS.md +3 -3
  76. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +0 -48
  77. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
  78. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
  79. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
  80. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
  81. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
  82. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +0 -161
  83. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
  84. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
  85. package/plugins/dsh/skills/clipboard-vision/SKILL.md +0 -48
  86. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
  87. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
  88. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
  89. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
  90. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
  91. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +0 -161
  92. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
  93. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
  94. package/plugins/opencode/skills/clipboard-vision/SKILL.md +0 -48
  95. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
  96. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
  97. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
  98. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
  99. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
  100. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +0 -161
  101. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
  102. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
  103. package/plugins/qoder/skills/clipboard-vision/SKILL.md +0 -48
  104. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
  105. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
  106. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
  107. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
  108. package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
  109. package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +0 -161
  110. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
  111. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
  112. package/skills/clipboard-vision/SKILL.md +0 -48
  113. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
  114. package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
  115. package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
  116. package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
  117. package/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
  118. package/skills/clipboard-vision/describe-clipboard.sh +0 -161
  119. package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
  120. package/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
@@ -1,273 +0,0 @@
1
- /**
2
- * @test REQ-379 privacy-safe clipboard vision
3
- * @intent Verify one-shot clipboard image analysis enforces local-first privacy boundaries
4
- * @covers AC-379-01, AC-379-02, AC-379-03, AC-379-04, AC-379-05, AC-379-06
5
- */
6
- const fs = require('fs');
7
- const crypto = require('crypto');
8
- const os = require('os');
9
- const path = require('path');
10
- const { spawnSync } = require('child_process');
11
-
12
- const ROOT = path.resolve(__dirname, '..');
13
- const BASH_SCRIPT = path.join(ROOT, 'scripts', 'clipboard-vision.sh');
14
- const POWERSHELL_SCRIPT = path.join(ROOT, 'scripts', 'clipboard-vision.ps1');
15
- const SECRET = 'cv-secret-must-never-leak';
16
-
17
- function writeExecutable(file, content) {
18
- fs.writeFileSync(file, content, { mode: 0o755 });
19
- }
20
-
21
- function createFixture() {
22
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clipboard-vision-test-'));
23
- const bin = path.join(root, 'bin');
24
- const temp = path.join(root, 'temp');
25
- fs.mkdirSync(bin);
26
- fs.mkdirSync(temp);
27
- const image = path.join(root, 'clipboard.png');
28
- fs.writeFileSync(image, Buffer.from('89504e470d0a1a0a', 'hex'));
29
- const clipboardCount = path.join(root, 'clipboard-count');
30
- const curlCount = path.join(root, 'curl-count');
31
- const curlArgs = path.join(root, 'curl-args');
32
- const curlBody = path.join(root, 'curl-body');
33
-
34
- writeExecutable(path.join(bin, 'pngpaste'), `#!/usr/bin/env bash
35
- set -euo pipefail
36
- printf x >> "${clipboardCount}"
37
- cp "${image}" "$1"
38
- `);
39
- writeExecutable(path.join(bin, 'curl'), `#!/usr/bin/env bash
40
- set -euo pipefail
41
- printf x >> "${curlCount}"
42
- printf '%s\n' "$@" > "${curlArgs}"
43
- while [ "$#" -gt 0 ]; do
44
- if [ "$1" = '--data-binary' ]; then cp "\${2#@}" "${curlBody}"; shift 2; continue; fi
45
- if [ "$1" = '--output' ]; then output="$2"; shift 2; continue; fi
46
- if [ "$1" = '--write-out' ]; then shift 2; continue; fi
47
- shift
48
- done
49
- if [ -n "\${CV_FAKE_RESPONSE:-}" ]; then
50
- printf '%s' "$CV_FAKE_RESPONSE" > "$output"
51
- else
52
- printf '%s' '{"choices":[{"message":{"content":"described safely"}}]}' > "$output"
53
- fi
54
- printf '%s' "\${CV_FAKE_STATUS:-200}"
55
- `);
56
-
57
- return { root, bin, temp, image, clipboardCount, curlCount, curlArgs, curlBody };
58
- }
59
-
60
- function run(fixture, args = [], env = {}) {
61
- return spawnSync('bash', [BASH_SCRIPT, ...args], {
62
- encoding: 'utf8',
63
- env: {
64
- ...process.env,
65
- PATH: `${fixture.bin}:${process.env.PATH}`,
66
- TMPDIR: fixture.temp,
67
- CLIPBOARD_VISION_MODEL: 'fixture-model',
68
- CLIPBOARD_VISION_API_KEY: SECRET,
69
- ...env,
70
- },
71
- });
72
- }
73
-
74
- function combined(result) {
75
- return `${result.stdout}${result.stderr}`;
76
- }
77
-
78
- function count(file) {
79
- return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').length : 0;
80
- }
81
-
82
- function consentToken(hostname, model, base = `https://${hostname}/v1`) {
83
- const url = new URL(base);
84
- const scheme = url.protocol.slice(0, -1).toLowerCase();
85
- const normalizedHost = url.hostname.toLowerCase();
86
- const port = url.port || (scheme === 'https' ? '443' : '80');
87
- const displayHost = normalizedHost.includes(':') ? `[${normalizedHost}]` : normalizedHost;
88
- const endpoint = `${scheme}://${displayHost}:${port}${url.pathname.replace(/\/+$/, '')}/chat/completions`;
89
- return crypto.createHash('sha256').update(`${endpoint}\n${model}`, 'utf8').digest('hex');
90
- }
91
-
92
- function assertNoTempFiles(fixture) {
93
- expect(fs.readdirSync(fixture.temp)).toEqual([]);
94
- }
95
-
96
- describe('clipboard-vision Bash client', () => {
97
- let fixture;
98
-
99
- beforeEach(() => {
100
- fixture = createFixture();
101
- });
102
-
103
- afterEach(() => {
104
- fs.rmSync(fixture.root, { recursive: true, force: true });
105
- });
106
-
107
- it('fails without model configuration before reading clipboard or calling network', () => {
108
- const result = run(fixture, [], { CLIPBOARD_VISION_MODEL: '' });
109
-
110
- expect(result.status).not.toBe(0);
111
- expect(count(fixture.clipboardCount)).toBe(0);
112
- expect(count(fixture.curlCount)).toBe(0);
113
- });
114
-
115
- it('rejects a non-loopback endpoint in local mode before reading clipboard', () => {
116
- const result = run(fixture, [], { CLIPBOARD_VISION_API_BASE: 'https://vision.example.test/v1' });
117
-
118
- expect(result.status).not.toBe(0);
119
- expect(count(fixture.clipboardCount)).toBe(0);
120
- expect(count(fixture.curlCount)).toBe(0);
121
- });
122
-
123
- it('requires both remote opt-in and destination-bound confirmation', () => {
124
- const endpoint = { CLIPBOARD_VISION_API_BASE: 'https://vision.example.test/v1' };
125
- const optInOnly = run(fixture, ['--allow-remote'], endpoint);
126
- const confirmationOnly = run(fixture, ['--confirm-remote', consentToken('vision.example.test', 'fixture-model')], endpoint);
127
-
128
- expect(optInOnly.status).not.toBe(0);
129
- expect(confirmationOnly.status).not.toBe(0);
130
- expect(count(fixture.clipboardCount)).toBe(0);
131
- expect(count(fixture.curlCount)).toBe(0);
132
- });
133
-
134
- it('prints disclosure and expected token before rejecting missing confirmation', () => {
135
- const result = run(fixture, ['--allow-remote'], {
136
- CLIPBOARD_VISION_API_BASE: 'https://vision.example.test/v1',
137
- });
138
-
139
- expect(result.status).not.toBe(0);
140
- expect(result.stderr).toContain('vision.example.test');
141
- expect(result.stderr).toContain('fixture-model');
142
- expect(result.stderr.toLowerCase()).toContain('image');
143
- expect(result.stderr).toContain(consentToken('vision.example.test', 'fixture-model'));
144
- expect(count(fixture.clipboardCount)).toBe(0);
145
- expect(count(fixture.curlCount)).toBe(0);
146
- });
147
-
148
- it('accepts the confirmation token bound to normalized hostname and exact model', () => {
149
- const token = consentToken('vision.example.test', 'fixture-model');
150
- const result = run(fixture, ['--allow-remote', '--confirm-remote', token], {
151
- CLIPBOARD_VISION_API_BASE: 'https://VISION.Example.Test/v1',
152
- });
153
-
154
- expect(result.status, combined(result)).toBe(0);
155
- expect(count(fixture.clipboardCount)).toBe(1);
156
- expect(count(fixture.curlCount)).toBe(1);
157
- });
158
-
159
- it('rejects a token for a changed destination before clipboard or network', () => {
160
- const staleToken = consentToken('old.example.test', 'fixture-model');
161
- const result = run(fixture, ['--allow-remote', '--confirm-remote', staleToken], {
162
- CLIPBOARD_VISION_API_BASE: 'https://vision.example.test/v1',
163
- });
164
-
165
- expect(result.status).not.toBe(0);
166
- expect(count(fixture.clipboardCount)).toBe(0);
167
- expect(count(fixture.curlCount)).toBe(0);
168
- });
169
-
170
- it('rejects a token for a changed model before clipboard or network', () => {
171
- const staleToken = consentToken('vision.example.test', 'old-model');
172
- const result = run(fixture, ['--allow-remote', '--confirm-remote', staleToken], {
173
- CLIPBOARD_VISION_API_BASE: 'https://vision.example.test/v1',
174
- });
175
-
176
- expect(result.status).not.toBe(0);
177
- expect(count(fixture.clipboardCount)).toBe(0);
178
- expect(count(fixture.curlCount)).toBe(0);
179
- });
180
-
181
- it('never exposes the API key in output or request JSON', () => {
182
- const result = run(fixture);
183
-
184
- expect(result.status, combined(result)).toBe(0);
185
- expect(combined(result)).not.toContain(SECRET);
186
- expect(fs.readFileSync(fixture.curlBody, 'utf8')).not.toContain(SECRET);
187
- });
188
-
189
- it('rejects oversized clipboard images before network access', () => {
190
- fs.writeFileSync(fixture.image, Buffer.alloc(33));
191
- const result = run(fixture, [], { CLIPBOARD_VISION_MAX_BYTES: '32' });
192
-
193
- expect(result.status).not.toBe(0);
194
- expect(count(fixture.curlCount)).toBe(0);
195
- assertNoTempFiles(fixture);
196
- });
197
-
198
- it('removes temporary files after success', () => {
199
- const result = run(fixture);
200
-
201
- expect(result.status, combined(result)).toBe(0);
202
- expect(result.stdout.trim()).toBe('described safely');
203
- assertNoTempFiles(fixture);
204
- });
205
-
206
- it('redacts API failure bodies and removes temporary files after failure', () => {
207
- const response = `upstream body contains ${SECRET}`;
208
- const result = run(fixture, [], { CV_FAKE_STATUS: '500', CV_FAKE_RESPONSE: response });
209
-
210
- expect(result.status).not.toBe(0);
211
- expect(combined(result)).not.toContain(response);
212
- expect(combined(result)).not.toContain(SECRET);
213
- assertNoTempFiles(fixture);
214
- });
215
-
216
- it('performs exactly one clipboard read and one request per explicit invocation', () => {
217
- const result = run(fixture);
218
-
219
- expect(result.status, combined(result)).toBe(0);
220
- expect(count(fixture.clipboardCount)).toBe(1);
221
- expect(count(fixture.curlCount)).toBe(1);
222
- });
223
- });
224
-
225
- describe('clipboard-vision PowerShell contract', () => {
226
- it('keeps credentials in the environment and enforces bound consent, bounds, and cleanup', () => {
227
- const content = fs.readFileSync(POWERSHELL_SCRIPT, 'utf8');
228
-
229
- expect(content).toMatch(/\$env:CLIPBOARD_VISION_API_KEY/);
230
- expect(content).toMatch(/AllowRemote/);
231
- expect(content).toMatch(/ConfirmRemote/);
232
- expect(content).toMatch(/SHA256/i);
233
- expect(content).toMatch(/CLIPBOARD_VISION_MAX_BYTES/);
234
- expect(content).toMatch(/finally\s*\{/i);
235
- expect(content).toMatch(/Remove-Item/);
236
- expect(content).not.toMatch(/https:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/);
237
- });
238
-
239
- it('disables HTTP redirects so loopback cannot forward the clipboard body remotely', () => {
240
- const content = fs.readFileSync(POWERSHELL_SCRIPT, 'utf8');
241
-
242
- expect(content).toMatch(/MaximumRedirection\s+0/);
243
- });
244
-
245
- it('validates an absolute HTTP(S) URI without credentials before disclosure or seams', () => {
246
- const content = fs.readFileSync(POWERSHELL_SCRIPT, 'utf8');
247
- const validationPosition = content.indexOf('IsAbsoluteUri');
248
- const disclosurePosition = content.indexOf('Remote disclosure');
249
- const clipboardPosition = content.indexOf('CLIPBOARD_VISION_TEST_IMAGE');
250
-
251
- expect(validationPosition).toBeGreaterThan(0);
252
- expect(content).toMatch(/Scheme[^\n]+https?/i);
253
- expect(content).toMatch(/UserInfo/);
254
- expect(validationPosition).toBeLessThan(disclosurePosition);
255
- expect(validationPosition).toBeLessThan(clipboardPosition);
256
- });
257
-
258
- it('ships a discoverable Windows-CI executable behavior harness', () => {
259
- expect(fs.existsSync(path.join(ROOT, '__tests__', 'clipboard-vision.Tests.ps1'))).toBe(true);
260
- });
261
-
262
- it('wires the discoverable PowerShell harness into windows-latest CI', () => {
263
- let repoRoot = ROOT;
264
- while (!fs.existsSync(path.join(repoRoot, '.github', 'workflows', 'cross-platform-ci.yml'))) {
265
- repoRoot = path.dirname(repoRoot);
266
- }
267
- const workflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'cross-platform-ci.yml'), 'utf8');
268
-
269
- expect(workflow).toContain('windows-latest');
270
- expect(workflow).toContain('clipboard-vision.Tests.ps1');
271
- expect(workflow).toMatch(/shell:\s*pwsh/);
272
- });
273
- });
@@ -1,115 +0,0 @@
1
- # describe-clipboard.ps1
2
- # Reads image from clipboard, sends to vision model API, writes description to stdout.
3
- # API key is read from CLIPBOARD_VISION_API_KEY environment variable.
4
- # WARNING: Do not enable -Verbose or Set-PSDebug -Trace 2 — the API key would be logged.
5
- param(
6
- [string]$OutputFile = "$env:TEMP\clipboard_desc.txt"
7
- )
8
-
9
- $ErrorActionPreference = "Stop"
10
-
11
- # Platform guard
12
- if (-not $IsWindows) {
13
- Write-Host "ERROR: This script requires Windows (uses .NET System.Windows.Forms)"
14
- exit 1
15
- }
16
-
17
- $apiKey = $env:CLIPBOARD_VISION_API_KEY
18
- if (-not $apiKey) {
19
- Write-Host "ERROR: CLIPBOARD_VISION_API_KEY environment variable not set"
20
- exit 1
21
- }
22
-
23
- $tempPng = $null
24
- $img = $null
25
-
26
- try {
27
- # --- Read clipboard image ---
28
- Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
29
- Add-Type -AssemblyName System.Drawing -ErrorAction Stop
30
-
31
- $img = [System.Windows.Forms.Clipboard]::GetImage()
32
- if (-not $img) {
33
- Write-Host "ERROR: No image found in clipboard"
34
- exit 1
35
- }
36
-
37
- # Save to temp PNG
38
- $tempPng = Join-Path $env:TEMP "clipboard_$(Get-Date -Format 'yyyyMMdd_HHmmss').png"
39
- $img.Save($tempPng, [System.Drawing.Imaging.ImageFormat]::Png)
40
-
41
- # Convert to base64
42
- $bytes = [System.IO.File]::ReadAllBytes($tempPng)
43
- $base64 = [System.Convert]::ToBase64String($bytes)
44
-
45
- # --- Build API request ---
46
- $apiBase = "https://lab.iwhalecloud.com/gpt-proxy"
47
- $model = "LOCAL/Qwen3.5-122B-A10B"
48
-
49
- $body = @{
50
- model = $model
51
- messages = @(
52
- @{
53
- role = "user"
54
- content = @(
55
- @{
56
- type = "text"
57
- text = "Describe this image in detail. Include all visible text, UI elements, code, error messages, objects, people, and any information conveyed. Be specific and thorough but concise. If it contains code or terminal output, quote it exactly."
58
- },
59
- @{
60
- type = "image_url"
61
- image_url = @{
62
- url = "data:image/png;base64,$base64"
63
- }
64
- }
65
- )
66
- }
67
- )
68
- max_tokens = 1024
69
- temperature = 0.1
70
- } | ConvertTo-Json -Depth 5
71
-
72
- # --- Call API ---
73
- $response = Invoke-RestMethod -Uri "$apiBase/chat/completions" `
74
- -Method Post `
75
- -Headers @{
76
- "Authorization" = "Bearer $apiKey"
77
- "Content-Type" = "application/json"
78
- } `
79
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
80
- -TimeoutSec 30
81
-
82
- # Validate response structure
83
- if (-not $response.choices -or -not $response.choices[0].message -or -not $response.choices[0].message.content) {
84
- Write-Host "ERROR: Unexpected API response structure"
85
- Write-Host "Response: $($response | ConvertTo-Json -Compress -Depth 2)"
86
- exit 1
87
- }
88
-
89
- $description = $response.choices[0].message.content
90
- } catch {
91
- Write-Host "ERROR: $_"
92
- exit 1
93
- } finally {
94
- # Cleanup temp file and GDI+ handle
95
- if ($tempPng -and (Test-Path $tempPng)) {
96
- Remove-Item $tempPng -Force -ErrorAction SilentlyContinue
97
- }
98
- if ($img) {
99
- $img.Dispose()
100
- }
101
- }
102
-
103
- if (-not $description) {
104
- Write-Host "ERROR: Empty response from vision model"
105
- exit 1
106
- }
107
-
108
- # Write to output file
109
- $description | Out-File -FilePath $OutputFile -Encoding UTF8 -Force
110
-
111
- # Output to stdout
112
- Write-Host "Clipboard image description saved to: $OutputFile"
113
- Write-Host "---"
114
- Write-Host $description
115
- Write-Host "---"
@@ -1,161 +0,0 @@
1
- #!/usr/bin/env bash
2
- # describe-clipboard.sh
3
- # Cross-platform: reads clipboard image, calls vision API, writes description.
4
- # Works on: WSL/Linux (xclip), macOS (osascript), Windows Git Bash (PowerShell)
5
- #
6
- # Requires CLIPBOARD_VISION_API_KEY environment variable.
7
- # WARNING: Do not run with `set -x` — the API key would be logged.
8
- set -euo pipefail
9
-
10
- OUTPUT_FILE="${1:-/tmp/clipboard_desc.txt}"
11
- API_KEY="${CLIPBOARD_VISION_API_KEY:-}"
12
- API_BASE="${CLIPBOARD_VISION_API_BASE:-https://lab.iwhalecloud.com/gpt-proxy}"
13
- MODEL="LOCAL/Qwen3.5-122B-A10B"
14
- TEMP_PNG=""
15
-
16
- if [ -z "$API_KEY" ]; then
17
- echo "ERROR: CLIPBOARD_VISION_API_KEY environment variable not set" >&2
18
- exit 1
19
- fi
20
-
21
- cleanup() {
22
- if [ -n "$TEMP_PNG" ] && [ -f "$TEMP_PNG" ]; then
23
- rm -f "$TEMP_PNG"
24
- fi
25
- }
26
- trap cleanup EXIT
27
-
28
- # --- Detect platform and read clipboard ---
29
- get_base64_from_clipboard() {
30
- local platform b64_cmd
31
- platform="$(uname -s)"
32
-
33
- if [ "$platform" = "Linux" ]; then
34
- b64_cmd="base64 -w 0"
35
- # WSL / Linux: use xclip or wl-paste
36
- if command -v xclip &>/dev/null; then
37
- TARGETS="$(xclip -selection clipboard -o -t TARGETS 2>/dev/null || echo "")"
38
- if echo "$TARGETS" | grep -qx "image/png"; then
39
- TEMP_PNG="$(mktemp /tmp/clipboard_XXXXXX.png)"
40
- xclip -selection clipboard -o -t image/png > "$TEMP_PNG"
41
- $b64_cmd "$TEMP_PNG"
42
- else
43
- echo "ERROR: No PNG image in clipboard (xclip targets: ${TARGETS:-none})" >&2
44
- exit 1
45
- fi
46
- elif command -v wl-paste &>/dev/null; then
47
- TEMP_PNG="$(mktemp /tmp/clipboard_XXXXXX.png)"
48
- wl-paste -t image/png > "$TEMP_PNG"
49
- $b64_cmd "$TEMP_PNG"
50
- else
51
- echo "ERROR: No clipboard tool found (install xclip or wl-clipboard)" >&2
52
- exit 1
53
- fi
54
- elif [ "$platform" = "MINGW64_NT" ] || [ "$platform" = "MSYS_NT" ] || [ "${OS:-}" = "Windows_NT" ]; then
55
- b64_cmd="base64 -w 0"
56
- # Windows Git Bash: delegate to PowerShell
57
- PS_EXE="/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
58
- if [ ! -f "$PS_EXE" ]; then
59
- PS_EXE="powershell.exe"
60
- fi
61
- TEMP_PNG="$(mktemp --suffix=.png)"
62
- WIN_TEMP="$(cygpath -w "$TEMP_PNG" 2>/dev/null || echo "$TEMP_PNG")"
63
- "$PS_EXE" -NoProfile -NonInteractive -Command "
64
- Add-Type -AssemblyName System.Windows.Forms;
65
- Add-Type -AssemblyName System.Drawing;
66
- \$img = [System.Windows.Forms.Clipboard]::GetImage();
67
- if (-not \$img) { Write-Host 'NO_IMAGE'; exit 1 };
68
- \$img.Save('$WIN_TEMP', [System.Drawing.Imaging.ImageFormat]::Png);
69
- Write-Host 'OK';
70
- " > /dev/null
71
- if [ ! -f "$TEMP_PNG" ] || [ ! -s "$TEMP_PNG" ]; then
72
- echo "ERROR: No image found in clipboard" >&2
73
- exit 1
74
- fi
75
- $b64_cmd "$TEMP_PNG"
76
- else
77
- # macOS
78
- b64_cmd="base64 -b 0"
79
- if command -v osascript &>/dev/null; then
80
- TEMP_PNG="$(mktemp /tmp/clipboard_XXXXXX.png)"
81
- if ! osascript -e "set theFile to (open for access POSIX file \"$TEMP_PNG\" with write permission)" \
82
- -e "write (the clipboard as «class PNGf») to theFile" \
83
- -e "close access theFile" 2>/dev/null; then
84
- echo "ERROR: Failed to read image from macOS clipboard" >&2
85
- exit 1
86
- fi
87
- if [ ! -s "$TEMP_PNG" ]; then
88
- echo "ERROR: No image in clipboard" >&2
89
- exit 1
90
- fi
91
- $b64_cmd "$TEMP_PNG"
92
- else
93
- echo "ERROR: Unsupported platform ($platform)" >&2
94
- exit 1
95
- fi
96
- fi
97
- }
98
-
99
- # --- Build and send API request via python3 (safe JSON construction) ---
100
- call_vision_api() {
101
- local b64="$1"
102
- python3 -c "
103
- import json, sys, os, urllib.request
104
-
105
- api_key = os.environ['CLIPBOARD_VISION_API_KEY']
106
- api_base = os.environ.get('CLIPBOARD_VISION_API_BASE', 'https://lab.iwhalecloud.com/gpt-proxy')
107
- model = 'LOCAL/Qwen3.5-122B-A10B'
108
-
109
- body = json.dumps({
110
- 'model': model,
111
- 'messages': [{
112
- 'role': 'user',
113
- 'content': [
114
- {'type': 'text', 'text': 'Describe this image in detail. Include all visible text, UI elements, code, error messages, objects, people, and any information conveyed. Be specific and thorough but concise. If it contains code or terminal output, quote it exactly.'},
115
- {'type': 'image_url', 'image_url': {'url': f'data:image/png;base64,{sys.argv[1]}'}}
116
- ]
117
- }],
118
- 'max_tokens': 1024,
119
- 'temperature': 0.1
120
- }).encode('utf-8')
121
-
122
- req = urllib.request.Request(
123
- f'{api_base}/chat/completions',
124
- data=body,
125
- headers={
126
- 'Authorization': f'Bearer {api_key}',
127
- 'Content-Type': 'application/json'
128
- }
129
- )
130
-
131
- try:
132
- with urllib.request.urlopen(req, timeout=30) as resp:
133
- data = json.loads(resp.read().decode('utf-8'))
134
- if not data.get('choices') or not data['choices'][0].get('message'):
135
- print('ERROR: Unexpected API response structure', file=sys.stderr)
136
- sys.exit(1)
137
- content = data['choices'][0]['message'].get('content', '')
138
- if not content:
139
- print('ERROR: Empty response from vision model', file=sys.stderr)
140
- sys.exit(1)
141
- print(content)
142
- except Exception as e:
143
- print(f'ERROR: API call failed: {e}', file=sys.stderr)
144
- sys.exit(1)
145
- " "$b64"
146
- }
147
-
148
- # --- Main ---
149
- echo "[clipboard-vision] Reading clipboard..." >&2
150
- B64="$(get_base64_from_clipboard)"
151
-
152
- echo "[clipboard-vision] Calling vision model ($MODEL)..." >&2
153
-
154
- DESCRIPTION="$(call_vision_api "$B64")"
155
-
156
- echo "$DESCRIPTION" > "$OUTPUT_FILE"
157
- echo "[clipboard-vision] Description saved to: $OUTPUT_FILE" >&2
158
-
159
- echo "---"
160
- echo "$DESCRIPTION"
161
- echo "---"
@@ -1,86 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [switch]$AllowRemote,
4
- [string]$ConfirmRemote
5
- )
6
-
7
- $ErrorActionPreference = 'Stop'
8
- $ApiBase = if ($env:CLIPBOARD_VISION_API_BASE) { $env:CLIPBOARD_VISION_API_BASE } else { 'http://127.0.0.1:11434/v1' }
9
- $Model = $env:CLIPBOARD_VISION_MODEL
10
- $ApiKey = $env:CLIPBOARD_VISION_API_KEY
11
- $MaxBytes = if ($env:CLIPBOARD_VISION_MAX_BYTES) { [long]$env:CLIPBOARD_VISION_MAX_BYTES } else { 10485760 }
12
-
13
- if (-not $Model) { throw 'CLIPBOARD_VISION_MODEL is required' }
14
- if ($MaxBytes -le 0) { throw 'CLIPBOARD_VISION_MAX_BYTES must be positive' }
15
- $Endpoint = $null
16
- if (-not [Uri]::TryCreate($ApiBase, [UriKind]::Absolute, [ref]$Endpoint) -or
17
- -not $Endpoint.IsAbsoluteUri -or
18
- $Endpoint.Scheme -notin @('http', 'https') -or
19
- $Endpoint.UserInfo.Length -ne 0 -or
20
- $Endpoint.Query.Length -ne 0 -or
21
- $Endpoint.Fragment.Length -ne 0) {
22
- throw 'CLIPBOARD_VISION_API_BASE must be an absolute HTTP(S) URI without credentials'
23
- }
24
- $Address = $null
25
- $LiteralAddress = [Net.IPAddress]::TryParse($Endpoint.Host.Trim('[', ']'), [ref]$Address)
26
- $Loopback = $LiteralAddress -and [Net.IPAddress]::IsLoopback($Address)
27
- $NormalizedHost = $Endpoint.Host.Trim('[', ']').ToLowerInvariant()
28
- $EffectivePort = if ($Endpoint.IsDefaultPort) { if ($Endpoint.Scheme -eq 'https') { 443 } else { 80 } } else { $Endpoint.Port }
29
- $DisplayHost = if ($NormalizedHost.Contains(':')) { "[$NormalizedHost]" } else { $NormalizedHost }
30
- $BasePath = $Endpoint.AbsolutePath.TrimEnd('/')
31
- $EffectiveEndpoint = "$($Endpoint.Scheme.ToLowerInvariant())://$DisplayHost`:$EffectivePort$BasePath/chat/completions"
32
- if (-not $Loopback) {
33
- $ConsentBytes = [Text.Encoding]::UTF8.GetBytes("$EffectiveEndpoint`n$Model")
34
- $Hasher = [Security.Cryptography.SHA256]::Create()
35
- try { $ExpectedConfirmation = ([BitConverter]::ToString($Hasher.ComputeHash($ConsentBytes))).Replace('-', '').ToLowerInvariant() } finally { $Hasher.Dispose() }
36
- [Console]::Error.WriteLine("Remote disclosure: clipboard image data will be sent to $EffectiveEndpoint using model $Model.")
37
- [Console]::Error.WriteLine("Confirmation token for this canonical endpoint and model: $ExpectedConfirmation")
38
- if (-not $AllowRemote -or $ConfirmRemote -cne $ExpectedConfirmation) {
39
- throw 'Remote endpoint requires -AllowRemote and the disclosed -ConfirmRemote token'
40
- }
41
- }
42
-
43
- $TempDirectory = Join-Path ([IO.Path]::GetTempPath()) ("clipboard-vision-" + [Guid]::NewGuid())
44
- $ImagePath = Join-Path $TempDirectory 'clipboard.png'
45
- try {
46
- New-Item -ItemType Directory -Path $TempDirectory | Out-Null
47
- if ($env:CLIPBOARD_VISION_TEST_IMAGE) {
48
- Copy-Item -LiteralPath $env:CLIPBOARD_VISION_TEST_IMAGE -Destination $ImagePath
49
- } else {
50
- Add-Type -AssemblyName System.Windows.Forms
51
- Add-Type -AssemblyName System.Drawing
52
- $Image = [Windows.Forms.Clipboard]::GetImage()
53
- if ($null -eq $Image) { throw 'Clipboard does not contain an image' }
54
- try { $Image.Save($ImagePath, [Drawing.Imaging.ImageFormat]::Png) } finally { $Image.Dispose() }
55
- }
56
- if ((Get-Item $ImagePath).Length -gt $MaxBytes) { throw "Clipboard image exceeds $MaxBytes bytes" }
57
-
58
- $Encoded = [Convert]::ToBase64String([IO.File]::ReadAllBytes($ImagePath))
59
- $Body = @{
60
- model = $Model
61
- messages = @(@{ role = 'user'; content = @(
62
- @{ type = 'text'; text = 'Describe this clipboard image.' },
63
- @{ type = 'image_url'; image_url = @{ url = "data:image/png;base64,$Encoded" } }
64
- ) })
65
- } | ConvertTo-Json -Depth 8 -Compress
66
- $Headers = @{}
67
- if ($ApiKey) { $Headers.Authorization = "Bearer $ApiKey" }
68
- try {
69
- if ($env:CLIPBOARD_VISION_TEST_HTTP_CAPTURE) {
70
- Set-Content -LiteralPath $env:CLIPBOARD_VISION_TEST_HTTP_CAPTURE -Value $Body -Encoding utf8
71
- if ($env:CV_TEST_HTTP_FAILURE) { throw 'test HTTP failure body secret' }
72
- if ($env:CV_TEST_MALFORMED_JSON) { $Response = $null }
73
- elseif ($env:CV_TEST_NON_STRING) { $Response = @{ choices = @(@{ message = @{ content = @{ value = 1 } } }) } }
74
- else { $Response = @{ choices = @(@{ message = @{ content = 'test description' } }) } }
75
- } else {
76
- $Response = Invoke-RestMethod -Method Post -Uri $EffectiveEndpoint -Headers $Headers -ContentType 'application/json' -Body $Body -MaximumRedirection 0
77
- }
78
- } catch {
79
- throw 'Vision API request failed (response body redacted)'
80
- }
81
- $Result = $Response.choices[0].message.content
82
- if ($Result -isnot [string]) { throw 'Invalid vision API response (body redacted)' }
83
- $Result
84
- } finally {
85
- Remove-Item -LiteralPath $TempDirectory -Recurse -Force -ErrorAction SilentlyContinue
86
- }