@comate/zulu 1.4.0-beta.4 → 1.4.0-beta.6

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 (53) hide show
  1. package/comate-engine/assets/skills/auto-commit/SKILL.md +2 -0
  2. package/comate-engine/assets/skills/auto-commit-sandbox-comate/SKILL.md +2 -2
  3. package/comate-engine/assets/skills/code-review/SKILL.md +6 -5
  4. package/comate-engine/assets/skills/code-review/agents/custom-reviewer.md +2 -2
  5. package/comate-engine/assets/skills/code-review/agents/meta-reviewer.md +2 -2
  6. package/comate-engine/assets/skills/code-review/agents/style-reviewer.md +72 -10
  7. package/comate-engine/assets/skills/code-review/references/dispatch-template.md +12 -12
  8. package/comate-engine/assets/skills/code-review/references/rules/Java/JAVA_STYLE_RULES.md +11 -5
  9. package/comate-engine/assets/skills/code-security/SKILL.md +110 -41
  10. package/comate-engine/assets/skills/code-security/references/credential_hosting.md +190 -28
  11. package/comate-engine/assets/skills/code-security/references/vul_analysis-go_sql_injection.md +149 -0
  12. package/comate-engine/assets/skills/code-security/references/vul_analysis-java_sql_injection.md +185 -0
  13. package/comate-engine/assets/skills/code-security/references/vul_analysis-php_sql_injection.md +147 -0
  14. package/comate-engine/assets/skills/code-security/references/vul_analysis-python_sql_injection.md +143 -0
  15. package/comate-engine/assets/skills/code-security/references/vul_repair-go_sql_injection.md +2 -2
  16. package/comate-engine/assets/skills/code-security/references/vul_repair-sca.md +225 -0
  17. package/comate-engine/assets/skills/code-security/scripts/credential_hosting.py +12 -10
  18. package/comate-engine/assets/skills/code-security/scripts/credential_open_page.py +125 -0
  19. package/comate-engine/assets/skills/code-security/scripts/credential_poll.py +12 -9
  20. package/comate-engine/assets/skills/code-security/scripts/credential_url.py +81 -0
  21. package/comate-engine/assets/skills/code-security/scripts/ducc/get_claude_session_id.sh +33 -0
  22. package/comate-engine/assets/skills/code-security/scripts/ducc/open_browser.py +191 -0
  23. package/comate-engine/assets/skills/code-security/scripts/parse_scan_result.py +99 -16
  24. package/comate-engine/assets/skills/code-security/scripts/repair_vulnerability.py +66 -13
  25. package/comate-engine/assets/skills/code-security/scripts/scan_vulnerability.py +44 -12
  26. package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/SKILL.md +8 -8
  27. package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/long_running_task.md +0 -15
  28. package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/testing_strategy.md +1 -1
  29. package/comate-engine/assets/skills/create-image/SKILL.md +197 -206
  30. package/comate-engine/assets/skills/create-image/scripts/generate-image.ps1 +213 -0
  31. package/comate-engine/assets/skills/create-image/scripts/generate-image.sh +322 -0
  32. package/comate-engine/assets/skills/create-subagent/SKILL.md +23 -5
  33. package/comate-engine/fallbackServer.js +1 -1
  34. package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.js +1 -1
  35. package/comate-engine/server.js +89 -66
  36. package/dist/bundle/index.js +3 -3
  37. package/package.json +1 -1
  38. package/scripts/postinstall.js +4 -3
  39. package/comate-engine/assets/skills/code-review/evals/SKILL.md +0 -334
  40. package/comate-engine/assets/skills/code-review/evals/agents/gt-generator.md +0 -76
  41. package/comate-engine/assets/skills/code-review/evals/agents/miner.md +0 -87
  42. package/comate-engine/assets/skills/code-review/evals/agents/score-judge.md +0 -168
  43. package/comate-engine/assets/skills/code-review/evals/references/cli-query-template.md +0 -114
  44. package/comate-engine/assets/skills/code-review/evals/references/gt-schema.md +0 -77
  45. package/comate-engine/assets/skills/code-review/references/custom-rules/RULE_TEMPLATE.md +0 -141
  46. /package/comate-engine/assets/commands/{code-review-comate.md → code-review.md} +0 -0
  47. /package/comate-engine/assets/commands/{debug-comate.md → debug.md} +0 -0
  48. /package/comate-engine/assets/commands/{unit-test-comate.md → unit-test.md} +0 -0
  49. /package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/backend_dev.md +0 -0
  50. /package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/env_setup.md +0 -0
  51. /package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/frontend_dev.md +0 -0
  52. /package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/references/git_operations.md +0 -0
  53. /package/comate-engine/assets/skills/{create-automation-tasks-comate → create-automation}/scripts/check_config.py +0 -0
@@ -0,0 +1,213 @@
1
+ # generate-image.ps1 — Nano Banana Pro (Gemini 3 Pro Image) wrapper for Windows
2
+ #
3
+ # Design goals:
4
+ # - Idempotent: re-running is safe; existing `images\` dir is reused.
5
+ # - Agent-friendly errors: every failure writes a single-line tagged message
6
+ # to stderr: `[generate-image:<stage>] <msg>`.
7
+ #
8
+ # Exit codes mirror the bash script:
9
+ # 0 success (absolute output path on stdout)
10
+ # 2 bad args 3 missing dep 4 missing env
11
+ # 5 input file error 10 HTTP/network 11 API error payload
12
+ # 12 no image data 13 local I/O error
13
+
14
+ [CmdletBinding()]
15
+ param(
16
+ [Parameter(Mandatory=$true)] [string] $Prompt,
17
+ [Parameter(Mandatory=$true)] [string] $Output,
18
+ [string] $InputPath = "",
19
+ [ValidateSet("1K","2K","4K","")] [string] $Resolution = "",
20
+ [ValidateSet("1:1","16:9","9:16","4:3","3:4","3:2","2:3","21:9")] [string] $AspectRatio = "1:1"
21
+ )
22
+
23
+ $ErrorActionPreference = "Stop"
24
+
25
+ function Write-Err([string]$Stage, [string]$Msg) {
26
+ [Console]::Error.WriteLine("[generate-image:{0}] {1}" -f $Stage, $Msg)
27
+ }
28
+ function Die([string]$Stage, [string]$Msg, [int]$Code) {
29
+ Write-Err $Stage $Msg
30
+ exit $Code
31
+ }
32
+
33
+ $ApiUrl = "https://comate.baidu-int.com/api/aidevops/autocomate/rest/autowork/v1/generate-image"
34
+ $OutDir = "images"
35
+
36
+ # ---- arg validation ---------------------------------------------------------
37
+ if ($Output -match '[\\/]') { Die "args" "-Output must be a filename only, not a path: $Output" 2 }
38
+
39
+ # ---- dependency / env check -------------------------------------------------
40
+ if (-not (Get-Command curl.exe -ErrorAction SilentlyContinue)) {
41
+ Die "deps" "curl.exe not found (requires Windows 10 1803+)" 3
42
+ }
43
+
44
+ function Resolve-LoginName {
45
+ if ($env:COMATE_USERNAME_ENCRYPTED) { return $env:COMATE_USERNAME_ENCRYPTED }
46
+ $homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME }
47
+ $f = Join-Path $homeDir ".comate/login"
48
+ if (-not (Test-Path -LiteralPath $f)) { return $null }
49
+ try {
50
+ $token = ((Get-Content -Raw -LiteralPath $f).Trim()) -replace '^Bearer-',''
51
+ $parts = $token.Split('.')
52
+ if ($parts.Count -lt 2) { return $null }
53
+ $p = $parts[1].Replace('-','+').Replace('_','/')
54
+ switch ($p.Length % 4) { 2 { $p += '==' } 3 { $p += '=' } }
55
+ $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($p))
56
+ return ($json | ConvertFrom-Json).content.identity
57
+ } catch { return $null }
58
+ }
59
+
60
+ $LoginName = Resolve-LoginName
61
+ if (-not $LoginName) {
62
+ Die "env" "COMATE_USERNAME_ENCRYPTED not set and ~/.comate/login unavailable/unparseable" 4
63
+ }
64
+
65
+ # ---- input file check -------------------------------------------------------
66
+ if ($InputPath) {
67
+ if (-not (Test-Path -LiteralPath $InputPath -PathType Leaf)) {
68
+ Die "input" "input image not found: $InputPath" 5
69
+ }
70
+ $inItem = Get-Item -LiteralPath $InputPath
71
+ if ($inItem.Length -le 0) { Die "input" "input image is empty: $InputPath" 5 }
72
+ }
73
+
74
+ # ---- output dir (idempotent) ------------------------------------------------
75
+ if (Test-Path -LiteralPath $OutDir) {
76
+ if (-not (Test-Path -LiteralPath $OutDir -PathType Container)) {
77
+ Die "output" "'$OutDir' exists but is not a directory" 13
78
+ }
79
+ } else {
80
+ try { New-Item -ItemType Directory -Path $OutDir -ErrorAction Stop | Out-Null }
81
+ catch { Die "output" "failed to create dir '$OutDir': $($_.Exception.Message)" 13 }
82
+ }
83
+
84
+ # ---- resolution auto-detect (image-to-image only) ---------------------------
85
+ if ($InputPath -and -not $Resolution) {
86
+ try {
87
+ Add-Type -AssemblyName System.Drawing
88
+ $img = [System.Drawing.Image]::FromFile((Resolve-Path -LiteralPath $InputPath))
89
+ $maxDim = [Math]::Max($img.Width, $img.Height)
90
+ $img.Dispose()
91
+ $Resolution = if ($maxDim -ge 3000) { "4K" } elseif ($maxDim -ge 1500) { "2K" } else { "1K" }
92
+ } catch {
93
+ Write-Err "input" "resolution auto-detect failed ($($_.Exception.Message)); falling back to 1K"
94
+ $Resolution = "1K"
95
+ }
96
+ }
97
+ if (-not $Resolution) { $Resolution = "1K" }
98
+
99
+ # ---- build payload ----------------------------------------------------------
100
+ $tmpFile = Join-Path $env:TEMP ("genimg-" + [guid]::NewGuid().ToString() + ".json")
101
+ $tmpResp = Join-Path $env:TEMP ("genimg-r-" + [guid]::NewGuid().ToString() + ".json")
102
+
103
+ try {
104
+ if ($InputPath) {
105
+ try {
106
+ $bytes = [IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $InputPath))
107
+ $b64 = [Convert]::ToBase64String($bytes)
108
+ } catch {
109
+ Die "input" "base64 encoding failed: $($_.Exception.Message)" 5
110
+ }
111
+ $mime = switch -Regex ($InputPath) {
112
+ '\.jpe?g$' { 'image/jpeg'; break }
113
+ '\.webp$' { 'image/webp'; break }
114
+ default { 'image/png' }
115
+ }
116
+ $payload = @{
117
+ contents = @(@{
118
+ role = "USER"
119
+ parts = @(
120
+ @{ inline_data = @{ mime_type = $mime; data = $b64 } },
121
+ @{ text = $Prompt }
122
+ )
123
+ })
124
+ resolution = $Resolution
125
+ aspectRatio = $AspectRatio
126
+ }
127
+ } else {
128
+ $payload = @{
129
+ contents = @(@{
130
+ role = "USER"
131
+ parts = @(@{ text = $Prompt })
132
+ })
133
+ resolution = $Resolution
134
+ aspectRatio = $AspectRatio
135
+ }
136
+ }
137
+
138
+ try {
139
+ $payload | ConvertTo-Json -Depth 10 -Compress |
140
+ Set-Content -Path $tmpFile -Encoding UTF8 -NoNewline -ErrorAction Stop
141
+ } catch {
142
+ Die "payload" "failed to write request JSON: $($_.Exception.Message)" 13
143
+ }
144
+
145
+ # ---- HTTP call (capture status + body separately) -----------------------
146
+ $httpCode = ""
147
+ try {
148
+ $httpCode = & curl.exe -sS -o "$tmpResp" -w "%{http_code}" -X POST $ApiUrl `
149
+ -H "Content-Type: application/json" `
150
+ -H "login-name: $LoginName" `
151
+ -d "@$tmpFile" 2>&1
152
+ } catch {
153
+ Die "http" "curl invocation failed: $($_.Exception.Message)" 10
154
+ }
155
+ if ($LASTEXITCODE -ne 0) {
156
+ Die "http" "curl exit=$LASTEXITCODE output=$httpCode" 10
157
+ }
158
+ if ($httpCode -notmatch '^2\d\d$') {
159
+ $snippet = ""
160
+ if (Test-Path -LiteralPath $tmpResp) {
161
+ $snippet = (Get-Content -LiteralPath $tmpResp -Raw -ErrorAction SilentlyContinue)
162
+ if ($snippet.Length -gt 2000) { $snippet = $snippet.Substring(0,2000) }
163
+ $snippet = $snippet -replace "\r?\n"," "
164
+ }
165
+ Write-Err "http" "HTTP $httpCode from API"
166
+ Write-Err "http" "body: $snippet"
167
+ exit 10
168
+ }
169
+
170
+ # ---- parse response -----------------------------------------------------
171
+ $raw = Get-Content -LiteralPath $tmpResp -Raw
172
+ try {
173
+ $response = $raw | ConvertFrom-Json -ErrorAction Stop
174
+ } catch {
175
+ $snip = if ($raw.Length -gt 2000) { $raw.Substring(0,2000) } else { $raw }
176
+ Write-Err "api" "response is not valid JSON"
177
+ Write-Err "api" "body: $($snip -replace '\r?\n',' ')"
178
+ exit 11
179
+ }
180
+
181
+ if ($response.error) {
182
+ $msg = if ($response.error.message) { $response.error.message } else { ($response.error | ConvertTo-Json -Compress) }
183
+ Die "api" "server returned error: $msg" 11
184
+ }
185
+
186
+ $imgPart = $response.candidates[0].content.parts |
187
+ Where-Object { $_.inlineData } | Select-Object -First 1
188
+ if (-not $imgPart) {
189
+ $text = ($response.candidates[0].content.parts |
190
+ Where-Object { $_.text } | Select-Object -ExpandProperty text) -join " "
191
+ if ($text) { Write-Err "api" "no image returned; model said: $text" }
192
+ $snip = if ($raw.Length -gt 2000) { $raw.Substring(0,2000) } else { $raw }
193
+ Write-Err "api" "no inlineData in response. body: $($snip -replace '\r?\n',' ')"
194
+ exit 12
195
+ }
196
+
197
+ # ---- write output -------------------------------------------------------
198
+ $outPath = Join-Path (Get-Location) (Join-Path $OutDir $Output)
199
+ try {
200
+ [IO.File]::WriteAllBytes($outPath, [Convert]::FromBase64String($imgPart.inlineData.data))
201
+ } catch {
202
+ Die "write" "failed to write output file: $($_.Exception.Message)" 13
203
+ }
204
+
205
+ if (-not (Test-Path -LiteralPath $outPath) -or (Get-Item -LiteralPath $outPath).Length -le 0) {
206
+ Die "write" "output file empty after decode: $outPath" 13
207
+ }
208
+
209
+ Write-Output $outPath
210
+ }
211
+ finally {
212
+ Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile, $tmpResp
213
+ }
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # generate-image.sh — Nano Banana Pro (Gemini 3 Pro Image) wrapper for macOS/Linux
4
+ #
5
+ # Usage:
6
+ # generate-image.sh --prompt "..." --output name.png \
7
+ # [--input path/to/ref.png] \
8
+ # [--resolution 1K|2K|4K] \
9
+ # [--aspect-ratio 1:1|16:9|9:16|4:3|3:4|3:2|2:3|21:9]
10
+ #
11
+ # Design goals:
12
+ # - Idempotent: safe to run repeatedly; existing `images/` dir is fine.
13
+ # - Agent-friendly errors: every failure path writes a single-line tagged
14
+ # message to stderr prefixed with `[generate-image:<stage>]` so the caller
15
+ # can pattern-match and react.
16
+ # - Portable: requires only curl + base64 + (python3 OR jq). No hard jq dep.
17
+ #
18
+ # Exit codes:
19
+ # 0 success — absolute output path on stdout
20
+ # 2 bad CLI args
21
+ # 3 missing dependency (curl/base64/python3|jq)
22
+ # 4 missing/invalid env (COMATE_USERNAME_ENCRYPTED)
23
+ # 5 input file missing / unreadable
24
+ # 10 HTTP/network failure
25
+ # 11 API returned error payload
26
+ # 12 API response had no image data
27
+ # 13 output file empty after write
28
+
29
+ set -uo pipefail
30
+
31
+ err() { printf '[generate-image:%s] %s\n' "$1" "$2" >&2; }
32
+ die() { err "$1" "$2"; exit "$3"; }
33
+
34
+ API_URL="https://comate.baidu-int.com/api/aidevops/autocomate/rest/autowork/v1/generate-image"
35
+ OUT_DIR="images"
36
+
37
+ PROMPT=""
38
+ OUTPUT=""
39
+ INPUT=""
40
+ RESOLUTION=""
41
+ ASPECT="1:1"
42
+
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ -p|--prompt) PROMPT="${2:-}"; shift 2 ;;
46
+ -o|--output) OUTPUT="${2:-}"; shift 2 ;;
47
+ -i|--input) INPUT="${2:-}"; shift 2 ;;
48
+ -r|--resolution) RESOLUTION="${2:-}"; shift 2 ;;
49
+ -a|--aspect-ratio) ASPECT="${2:-}"; shift 2 ;;
50
+ -h|--help)
51
+ sed -n '3,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
52
+ *) die "args" "unknown argument: $1" 2 ;;
53
+ esac
54
+ done
55
+
56
+ # ---- arg validation ---------------------------------------------------------
57
+ [[ -z "$PROMPT" ]] && die "args" "--prompt is required" 2
58
+ [[ -z "$OUTPUT" ]] && die "args" "--output is required" 2
59
+ [[ "$OUTPUT" == */* ]] && die "args" "--output must be a filename only, not a path: $OUTPUT" 2
60
+
61
+ if [[ -n "$RESOLUTION" ]]; then
62
+ case "$RESOLUTION" in 1K|2K|4K) ;; *) die "args" "invalid --resolution: $RESOLUTION (expect 1K|2K|4K)" 2 ;; esac
63
+ fi
64
+ case "$ASPECT" in
65
+ 1:1|16:9|9:16|4:3|3:4|3:2|2:3|21:9) ;;
66
+ *) die "args" "invalid --aspect-ratio: $ASPECT" 2 ;;
67
+ esac
68
+
69
+ # ---- dependency check -------------------------------------------------------
70
+ for bin in curl base64; do
71
+ command -v "$bin" >/dev/null 2>&1 || die "deps" "required binary not found: $bin" 3
72
+ done
73
+ # JSON tool: prefer python3 (universally available on Linux/macOS), fallback jq.
74
+ JSON_TOOL=""
75
+ if command -v python3 >/dev/null 2>&1; then
76
+ JSON_TOOL="python3"
77
+ elif command -v jq >/dev/null 2>&1; then
78
+ JSON_TOOL="jq"
79
+ else
80
+ die "deps" "need either 'python3' or 'jq' for JSON handling (install one)" 3
81
+ fi
82
+
83
+ # ---- JSON helpers (branch on $JSON_TOOL) ------------------------------------
84
+ # build_payload_text <outfile> — uses PROMPT/RESOLUTION/ASPECT env
85
+ # build_payload_image <outfile> <b64> <mime> — uses PROMPT/RESOLUTION/ASPECT env
86
+ # parse_error <respfile> -> prints error message or empty
87
+ # parse_image <respfile> -> prints base64 data or empty
88
+ # parse_text <respfile> -> prints concatenated text parts (reasoning)
89
+ # parse_jwt_identity <raw-b64-decoded-json on stdin> -> prints identity
90
+
91
+ build_payload_text() {
92
+ local out="$1"
93
+ if [[ "$JSON_TOOL" == "python3" ]]; then
94
+ PROMPT="$PROMPT" RES="$RESOLUTION" ASP="$ASPECT" python3 - <<'PY' > "$out"
95
+ import json, os
96
+ print(json.dumps({
97
+ "contents":[{"role":"USER","parts":[{"text":os.environ["PROMPT"]}]}],
98
+ "resolution":os.environ["RES"],
99
+ "aspectRatio":os.environ["ASP"]
100
+ }))
101
+ PY
102
+ else
103
+ jq -n --arg t "$PROMPT" --arg r "$RESOLUTION" --arg a "$ASPECT" \
104
+ '{contents:[{role:"USER",parts:[{text:$t}]}],resolution:$r,aspectRatio:$a}' > "$out"
105
+ fi
106
+ }
107
+
108
+ # NOTE: base64 payloads can exceed ARG_MAX (E2BIG) if passed via argv/env,
109
+ # so we always pass the base64 via a file path.
110
+ build_payload_image() {
111
+ local out="$1" b64_file="$2" mime="$3"
112
+ if [[ "$JSON_TOOL" == "python3" ]]; then
113
+ PROMPT="$PROMPT" RES="$RESOLUTION" ASP="$ASPECT" MIME="$mime" B64_FILE="$b64_file" python3 - <<'PY' > "$out"
114
+ import json, os
115
+ with open(os.environ["B64_FILE"], "r") as f:
116
+ b64 = f.read()
117
+ print(json.dumps({
118
+ "contents":[{"role":"USER","parts":[
119
+ {"inline_data":{"mime_type":os.environ["MIME"],"data":b64}},
120
+ {"text":os.environ["PROMPT"]}
121
+ ]}],
122
+ "resolution":os.environ["RES"],
123
+ "aspectRatio":os.environ["ASP"]
124
+ }))
125
+ PY
126
+ else
127
+ # --rawfile reads the whole file as a raw string, bypassing argv length limits.
128
+ jq -n --arg t "$PROMPT" --rawfile b "$b64_file" --arg m "$mime" --arg r "$RESOLUTION" --arg a "$ASPECT" \
129
+ '{contents:[{role:"USER",parts:[{inline_data:{mime_type:$m,data:$b}},{text:$t}]}],resolution:$r,aspectRatio:$a}' > "$out"
130
+ fi
131
+ }
132
+
133
+ parse_error() {
134
+ local f="$1"
135
+ if [[ "$JSON_TOOL" == "python3" ]]; then
136
+ python3 - "$f" <<'PY'
137
+ import json, sys
138
+ try:
139
+ d = json.load(open(sys.argv[1]))
140
+ e = d.get("error")
141
+ if not e: sys.exit(0)
142
+ if isinstance(e, dict):
143
+ print(e.get("message") or json.dumps(e, ensure_ascii=False))
144
+ else:
145
+ print(str(e))
146
+ except Exception:
147
+ pass
148
+ PY
149
+ else
150
+ jq -r 'if .error then (.error.message // (.error|tostring)) else empty end' "$f" 2>/dev/null
151
+ fi
152
+ }
153
+
154
+ parse_image() {
155
+ local f="$1"
156
+ if [[ "$JSON_TOOL" == "python3" ]]; then
157
+ python3 - "$f" <<'PY'
158
+ import json, sys
159
+ try:
160
+ d = json.load(open(sys.argv[1]))
161
+ for p in (d.get("candidates") or [{}])[0].get("content",{}).get("parts",[]) or []:
162
+ data = (p.get("inlineData") or {}).get("data") or (p.get("inline_data") or {}).get("data")
163
+ if data:
164
+ sys.stdout.write(data)
165
+ break
166
+ except Exception:
167
+ pass
168
+ PY
169
+ else
170
+ jq -r '.candidates[0].content.parts[]? | select(.inlineData // .inline_data) | (.inlineData // .inline_data).data' "$f" 2>/dev/null | head -1
171
+ fi
172
+ }
173
+
174
+ parse_text() {
175
+ local f="$1"
176
+ if [[ "$JSON_TOOL" == "python3" ]]; then
177
+ python3 - "$f" <<'PY'
178
+ import json, sys
179
+ try:
180
+ d = json.load(open(sys.argv[1]))
181
+ out=[]
182
+ for p in (d.get("candidates") or [{}])[0].get("content",{}).get("parts",[]) or []:
183
+ if "text" in p and p["text"]:
184
+ out.append(p["text"])
185
+ sys.stdout.write("\n".join(out))
186
+ except Exception:
187
+ pass
188
+ PY
189
+ else
190
+ jq -r '.candidates[0].content.parts[]? | select(.text) | .text' "$f" 2>/dev/null
191
+ fi
192
+ }
193
+
194
+ parse_jwt_identity_from_stdin() {
195
+ if [[ "$JSON_TOOL" == "python3" ]]; then
196
+ # Use -c (not heredoc) so stdin remains the piped JWT bytes, not the script.
197
+ python3 -c 'import json,sys
198
+ try:
199
+ d=json.load(sys.stdin)
200
+ sys.stdout.write((d.get("content") or {}).get("identity") or "")
201
+ except Exception:
202
+ pass'
203
+ else
204
+ jq -r '.content.identity // empty' 2>/dev/null
205
+ fi
206
+ }
207
+
208
+ # ---- resolve login-name (env > ~/.comate/login JWT) -------------------------
209
+ resolve_login_name() {
210
+ if [[ -n "${COMATE_USERNAME_ENCRYPTED:-}" ]]; then
211
+ printf '%s' "$COMATE_USERNAME_ENCRYPTED"; return 0
212
+ fi
213
+ local f="${HOME}/.comate/login"
214
+ [[ -f "$f" ]] || return 1
215
+ local token payload pad ident
216
+ token=$(tr -d '[:space:]' < "$f" | sed 's/^Bearer-//')
217
+ payload="${token#*.}"; payload="${payload%%.*}"
218
+ [[ -z "$payload" ]] && return 1
219
+ # base64url -> base64 + pad to multiple of 4
220
+ payload=$(printf '%s' "$payload" | tr '_-' '/+')
221
+ pad=$(( (4 - ${#payload} % 4) % 4 ))
222
+ while [[ $pad -gt 0 ]]; do payload+='='; pad=$((pad-1)); done
223
+ ident=$(printf '%s' "$payload" | base64 --decode 2>/dev/null | parse_jwt_identity_from_stdin)
224
+ [[ -n "$ident" ]] && { printf '%s' "$ident"; return 0; }
225
+ return 1
226
+ }
227
+
228
+ LOGIN_NAME=$(resolve_login_name) || die "env" "COMATE_USERNAME_ENCRYPTED not set and ~/.comate/login unavailable/unparseable" 4
229
+
230
+ if [[ -n "$INPUT" ]]; then
231
+ [[ -f "$INPUT" ]] || die "input" "input image not found: $INPUT" 5
232
+ [[ -r "$INPUT" ]] || die "input" "input image not readable: $INPUT" 5
233
+ [[ -s "$INPUT" ]] || die "input" "input image is empty: $INPUT" 5
234
+ fi
235
+
236
+ # ---- output dir (idempotent) ------------------------------------------------
237
+ if [[ -e "$OUT_DIR" && ! -d "$OUT_DIR" ]]; then
238
+ die "output" "'$OUT_DIR' exists but is not a directory" 13
239
+ fi
240
+ mkdir -p "$OUT_DIR" || die "output" "failed to create output dir: $OUT_DIR" 13
241
+
242
+ # ---- resolution auto-detect (image-to-image only) ---------------------------
243
+ if [[ -n "$INPUT" && -z "$RESOLUTION" ]]; then
244
+ MAX_DIM=0
245
+ if command -v sips >/dev/null 2>&1; then
246
+ MAX_DIM=$(sips -g pixelWidth -g pixelHeight "$INPUT" 2>/dev/null | awk '/pixel/{print $2}' | sort -rn | head -1 || echo 0)
247
+ elif command -v identify >/dev/null 2>&1; then
248
+ MAX_DIM=$(identify -format "%[fx:max(w,h)]" "$INPUT" 2>/dev/null || echo 0)
249
+ fi
250
+ if [[ "$MAX_DIM" =~ ^[0-9]+$ ]] && [[ "$MAX_DIM" -ge 3000 ]]; then RESOLUTION="4K"
251
+ elif [[ "$MAX_DIM" =~ ^[0-9]+$ ]] && [[ "$MAX_DIM" -ge 1500 ]]; then RESOLUTION="2K"
252
+ else RESOLUTION="1K"
253
+ fi
254
+ fi
255
+ RESOLUTION="${RESOLUTION:-1K}"
256
+
257
+ # ---- build payload ----------------------------------------------------------
258
+ TMP_JSON=$(mktemp -t genimg.XXXXXX) || die "tmp" "mktemp failed" 13
259
+ TMP_RESP=$(mktemp -t genimg-resp.XXXXXX) || die "tmp" "mktemp failed" 13
260
+ TMP_B64=$(mktemp -t genimg-b64.XXXXXX) || die "tmp" "mktemp failed" 13
261
+ trap 'rm -f "$TMP_JSON" "$TMP_RESP" "$TMP_B64"' EXIT
262
+
263
+ if [[ -n "$INPUT" ]]; then
264
+ # Write base64 to a file (not an env var / argv) to avoid ARG_MAX (E2BIG) for large images.
265
+ # macOS base64 supports `-i <file>`; GNU coreutils uses `base64 <file>`.
266
+ if ! base64 -i "$INPUT" 2>/dev/null | tr -d '\n' > "$TMP_B64"; then
267
+ base64 "$INPUT" 2>/dev/null | tr -d '\n' > "$TMP_B64" \
268
+ || die "input" "base64 encoding failed for: $INPUT" 5
269
+ fi
270
+ [[ -s "$TMP_B64" ]] || die "input" "base64 produced empty output for: $INPUT" 5
271
+ MIME="image/png"
272
+ case "${INPUT##*.}" in
273
+ jpg|jpeg|JPG|JPEG) MIME="image/jpeg" ;;
274
+ webp|WEBP) MIME="image/webp" ;;
275
+ esac
276
+ build_payload_image "$TMP_JSON" "$TMP_B64" "$MIME" || die "payload" "failed to build request JSON" 13
277
+ else
278
+ build_payload_text "$TMP_JSON" || die "payload" "failed to build request JSON" 13
279
+ fi
280
+ [[ -s "$TMP_JSON" ]] || die "payload" "payload file is empty" 13
281
+
282
+ # ---- HTTP call --------------------------------------------------------------
283
+ HTTP_CODE=$(curl -sS -o "$TMP_RESP" -w '%{http_code}' -X POST "$API_URL" \
284
+ -H "Content-Type: application/json" \
285
+ -H "login-name: ${LOGIN_NAME}" \
286
+ -d "@$TMP_JSON" 2>&1) || {
287
+ err "http" "curl failed: $HTTP_CODE"
288
+ exit 10
289
+ }
290
+
291
+ if [[ "$HTTP_CODE" != 2* ]]; then
292
+ BODY_SNIP=$(head -c 2000 "$TMP_RESP" | tr '\n' ' ')
293
+ err "http" "HTTP $HTTP_CODE from API"
294
+ err "http" "body: $BODY_SNIP"
295
+ exit 10
296
+ fi
297
+
298
+ # ---- parse response ---------------------------------------------------------
299
+ ERR_MSG=$(parse_error "$TMP_RESP")
300
+ if [[ -n "$ERR_MSG" ]]; then
301
+ err "api" "server returned error: $(printf '%s' "$ERR_MSG" | head -c 1000)"
302
+ exit 11
303
+ fi
304
+
305
+ B64=$(parse_image "$TMP_RESP")
306
+ if [[ -z "$B64" ]]; then
307
+ TEXT=$(parse_text "$TMP_RESP" | head -c 1000)
308
+ BODY_SNIP=$(head -c 2000 "$TMP_RESP" | tr '\n' ' ')
309
+ [[ -n "$TEXT" ]] && err "api" "no image returned; model said: $TEXT"
310
+ err "api" "no inlineData in response. body: $BODY_SNIP"
311
+ exit 12
312
+ fi
313
+
314
+ # ---- write output -----------------------------------------------------------
315
+ OUT_PATH="$OUT_DIR/$OUTPUT"
316
+ printf '%s' "$B64" | base64 --decode > "$OUT_PATH" || die "write" "base64 decode failed" 13
317
+
318
+ if [[ ! -s "$OUT_PATH" ]]; then
319
+ die "write" "output file is empty after decode: $OUT_PATH" 13
320
+ fi
321
+
322
+ printf '%s\n' "$(cd "$(dirname "$OUT_PATH")" && pwd)/$(basename "$OUT_PATH")"
@@ -30,6 +30,18 @@ When multiple subagents share the same name, the higher-priority location wins.
30
30
 
31
31
  **Personal subagents** (`~/.comate/agents/`): Personal agents available across all your projects.
32
32
 
33
+ ## Available Models
34
+
35
+ Subagents can optionally use a different model than the main agent via the `model` frontmatter field. Valid values are:
36
+
37
+ - `inherit` — use the main agent's current model (default when `model` is omitted or the value is unrecognized)
38
+ - `fast` — a cheaper, faster model
39
+ - An exact model name from the list below:
40
+
41
+ ${COMATE_AVAILABLE_MODELS}
42
+
43
+ Models listed with a `-Thinking` suffix support extended thinking/reasoning mode.
44
+
33
45
  ## Subagent File Format
34
46
 
35
47
  Create a `.md` file with YAML frontmatter and a markdown body (the system prompt):
@@ -38,6 +50,7 @@ Create a `.md` file with YAML frontmatter and a markdown body (the system prompt
38
50
  ---
39
51
  name: code-reviewer
40
52
  description: Reviews code for quality and best practices
53
+ model: Model this subagent uses
41
54
  ---
42
55
 
43
56
  You are a code reviewer. When invoked, analyze the code and provide
@@ -50,6 +63,7 @@ specific, actionable feedback on quality, security, and best practices.
50
63
  |-------|-------------|
51
64
  | `name` | Unique identifier (lowercase letters and hyphens only) |
52
65
  | `description` | When to delegate to this subagent (be specific!) |
66
+ | `model` | (Optional) Model this subagent uses. Must be one of the valid values from the **Available Models** section above: `inherit` (default), `fast`, or an exact model name from that list (including `-Thinking` variants). Unrecognized names fall back to `inherit`. |
53
67
 
54
68
  ## Writing Effective Descriptions
55
69
 
@@ -199,7 +213,11 @@ Below are candidate tools name, according to the user's query, decide the subage
199
213
  If you are not sure with some tools, ask her directly.
200
214
  Empty means only selecting default read-only tools; Lack of this key means selecting all tools
201
215
 
202
- ### Step 3: Create the File
216
+ ### Step 3: Clarify the Agent's Model
217
+ **If the user requests a specific model, confirm the exact name from the **Available Models** list above**.
218
+ Otherwise, tell her she can use `inherit` or `fast`.
219
+
220
+ ### Step 4: Create the File
203
221
 
204
222
  ```bash
205
223
  # For project-level
@@ -211,11 +229,11 @@ mkdir -p ~/.comate/agents
211
229
  touch ~/.comate/agents/my-agent.md
212
230
  ```
213
231
 
214
- ### Step 4: Define Configuration
232
+ ### Step 5: Define Configuration
215
233
 
216
- Write the frontmatter with the required fields (`name` and `description`).
234
+ Write the frontmatter with the required fields (`name` and `description`). Optionally add `model` (placed after `description`) using one of the valid values from the **Available Models** section: `inherit`, `fast`, or an exact model name from that list. If the user requests a specific model, confirm the exact name from the **Available Models** list.
217
235
 
218
- ### Step 5: Write the System Prompt
236
+ ### Step 6: Write the System Prompt
219
237
 
220
238
  The body becomes the system prompt. Be specific about:
221
239
  - What the agent should do when invoked
@@ -223,7 +241,7 @@ The body becomes the system prompt. Be specific about:
223
241
  - Output format and structure
224
242
  - Any constraints or guidelines
225
243
 
226
- ### Step 6: Test the Agent
244
+ ### Step 7: Test the Agent
227
245
 
228
246
  Ask the AI to use your new agent:
229
247