@hera-al/server 1.6.12 → 1.6.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.
- package/bundled/a2ui/SKILL.md +339 -0
- package/bundled/buongiorno/SKILL.md +151 -0
- package/bundled/council/SKILL.md +168 -0
- package/bundled/council/scripts/council.mjs +202 -0
- package/bundled/dreaming/SKILL.md +177 -0
- package/bundled/google-workspace/SKILL.md +229 -0
- package/bundled/google-workspace/scripts/auth.sh +87 -0
- package/bundled/google-workspace/scripts/calendar.sh +508 -0
- package/bundled/google-workspace/scripts/drive.sh +459 -0
- package/bundled/google-workspace/scripts/gmail.sh +452 -0
- package/bundled/humanizer/SKILL.md +488 -0
- package/bundled/librarian/SKILL.md +155 -0
- package/bundled/plasma/SKILL.md +1417 -0
- package/bundled/sera/SKILL.md +143 -0
- package/bundled/the-skill-guardian/SKILL.md +103 -0
- package/bundled/the-skill-guardian/scripts/scan.sh +314 -0
- package/bundled/unix-time/SKILL.md +58 -0
- package/bundled/wandering/SKILL.md +174 -0
- package/bundled/xai-search/SKILL.md +91 -0
- package/bundled/xai-search/scripts/search.sh +197 -0
- package/dist/a2ui/parser.d.ts +76 -0
- package/dist/a2ui/parser.js +1 -0
- package/dist/a2ui/types.d.ts +147 -0
- package/dist/a2ui/types.js +1 -0
- package/dist/a2ui/validator.d.ts +32 -0
- package/dist/a2ui/validator.js +1 -0
- package/dist/agent/agent-service.d.ts +17 -11
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/session-agent.d.ts +1 -1
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/session-error-handler.js +1 -1
- package/dist/commands/debuga2ui.d.ts +13 -0
- package/dist/commands/debuga2ui.js +1 -0
- package/dist/commands/debugdynamic.d.ts +13 -0
- package/dist/commands/debugdynamic.js +1 -0
- package/dist/commands/mcp.d.ts +6 -3
- package/dist/commands/mcp.js +1 -1
- package/dist/gateway/node-registry.d.ts +29 -1
- package/dist/gateway/node-registry.js +1 -1
- package/dist/installer/hera.js +1 -1
- package/dist/memory/concept-store.d.ts +109 -0
- package/dist/memory/concept-store.js +1 -0
- package/dist/nostromo/nostromo.js +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +1 -1
- package/dist/tools/a2ui-tools.d.ts +23 -0
- package/dist/tools/a2ui-tools.js +1 -0
- package/dist/tools/concept-tools.d.ts +3 -0
- package/dist/tools/concept-tools.js +1 -0
- package/dist/tools/dynamic-ui-tools.d.ts +25 -0
- package/dist/tools/dynamic-ui-tools.js +1 -0
- package/dist/tools/node-tools.js +1 -1
- package/dist/tools/plasma-client-tools.d.ts +28 -0
- package/dist/tools/plasma-client-tools.js +1 -0
- package/installationPkg/AGENTS.md +168 -22
- package/installationPkg/SOUL.md +56 -0
- package/installationPkg/TOOLS.md +126 -0
- package/installationPkg/USER.md +54 -1
- package/installationPkg/config.example.yaml +145 -34
- package/installationPkg/default-jobs.json +77 -0
- package/package.json +3 -2
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gmail API client — OAuth2 REST API v1
|
|
3
|
+
# Usage: gmail.sh <command> [options]
|
|
4
|
+
#
|
|
5
|
+
# Commands:
|
|
6
|
+
# list [--max N] List recent messages
|
|
7
|
+
# search "query" [--max N] Search messages (Gmail query syntax)
|
|
8
|
+
# read <messageId> [--format minimal|full] Read a message
|
|
9
|
+
# send --to ADDR --subject SUBJ --body TXT Send an email
|
|
10
|
+
# reply <messageId> --body TXT Reply to an email
|
|
11
|
+
# labels List all labels
|
|
12
|
+
# label <messageId> --add L --remove L Modify labels
|
|
13
|
+
# mark-read <messageId> Mark as read
|
|
14
|
+
# mark-unread <messageId> Mark as unread
|
|
15
|
+
# trash <messageId> Move to trash
|
|
16
|
+
# threads [--max N] List threads
|
|
17
|
+
# thread <threadId> Get thread
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# --- Config ---
|
|
22
|
+
API_BASE="https://gmail.googleapis.com/gmail/v1/users/me"
|
|
23
|
+
TOKEN_FILE="${HOME}/.gmab-google-token.json"
|
|
24
|
+
|
|
25
|
+
# --- Token management ---
|
|
26
|
+
get_access_token() {
|
|
27
|
+
local client_id="${GOOGLE_CLIENT_ID:-}"
|
|
28
|
+
local client_secret="${GOOGLE_CLIENT_SECRET:-}"
|
|
29
|
+
local refresh_token="${GOOGLE_REFRESH_TOKEN:-}"
|
|
30
|
+
|
|
31
|
+
# Try token file if env vars not set
|
|
32
|
+
if [[ -z "$refresh_token" && -f "$TOKEN_FILE" ]]; then
|
|
33
|
+
client_id=$(jq -r '.client_id // empty' "$TOKEN_FILE" 2>/dev/null || true)
|
|
34
|
+
client_secret=$(jq -r '.client_secret // empty' "$TOKEN_FILE" 2>/dev/null || true)
|
|
35
|
+
refresh_token=$(jq -r '.refresh_token // empty' "$TOKEN_FILE" 2>/dev/null || true)
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [[ -z "$client_id" || -z "$client_secret" || -z "$refresh_token" ]]; then
|
|
39
|
+
echo "ERROR: Google credentials not configured." >&2
|
|
40
|
+
echo "Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN" >&2
|
|
41
|
+
echo "Or run scripts/google/auth.sh to set up OAuth2." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Check cached token
|
|
46
|
+
if [[ -f "$TOKEN_FILE" ]]; then
|
|
47
|
+
local cached_token cached_expiry
|
|
48
|
+
cached_token=$(jq -r '.access_token // empty' "$TOKEN_FILE" 2>/dev/null || true)
|
|
49
|
+
cached_expiry=$(jq -r '.expires_at // 0' "$TOKEN_FILE" 2>/dev/null || true)
|
|
50
|
+
local now
|
|
51
|
+
now=$(date +%s)
|
|
52
|
+
if [[ -n "$cached_token" && "$cached_expiry" -gt "$now" ]]; then
|
|
53
|
+
echo "$cached_token"
|
|
54
|
+
return
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Refresh the token
|
|
59
|
+
local response
|
|
60
|
+
response=$(curl -s "https://oauth2.googleapis.com/token" \
|
|
61
|
+
-d "client_id=$client_id" \
|
|
62
|
+
-d "client_secret=$client_secret" \
|
|
63
|
+
-d "refresh_token=$refresh_token" \
|
|
64
|
+
-d "grant_type=refresh_token")
|
|
65
|
+
|
|
66
|
+
local access_token
|
|
67
|
+
access_token=$(echo "$response" | jq -r '.access_token // empty')
|
|
68
|
+
local expires_in
|
|
69
|
+
expires_in=$(echo "$response" | jq -r '.expires_in // 3600')
|
|
70
|
+
|
|
71
|
+
if [[ -z "$access_token" ]]; then
|
|
72
|
+
echo "ERROR: Failed to refresh access token" >&2
|
|
73
|
+
echo "$response" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Cache token
|
|
78
|
+
local expires_at
|
|
79
|
+
expires_at=$(($(date +%s) + expires_in - 60))
|
|
80
|
+
|
|
81
|
+
# Preserve existing fields, update token
|
|
82
|
+
if [[ -f "$TOKEN_FILE" ]]; then
|
|
83
|
+
local tmp
|
|
84
|
+
tmp=$(jq --arg at "$access_token" --argjson ea "$expires_at" \
|
|
85
|
+
'. + {access_token: $at, expires_at: $ea}' "$TOKEN_FILE")
|
|
86
|
+
echo "$tmp" > "$TOKEN_FILE"
|
|
87
|
+
else
|
|
88
|
+
jq -n \
|
|
89
|
+
--arg ci "$client_id" \
|
|
90
|
+
--arg cs "$client_secret" \
|
|
91
|
+
--arg rt "$refresh_token" \
|
|
92
|
+
--arg at "$access_token" \
|
|
93
|
+
--argjson ea "$expires_at" \
|
|
94
|
+
'{client_id: $ci, client_secret: $cs, refresh_token: $rt, access_token: $at, expires_at: $ea}' \
|
|
95
|
+
> "$TOKEN_FILE"
|
|
96
|
+
fi
|
|
97
|
+
chmod 600 "$TOKEN_FILE"
|
|
98
|
+
|
|
99
|
+
echo "$access_token"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
api_get() {
|
|
103
|
+
local path="$1"
|
|
104
|
+
local token
|
|
105
|
+
token=$(get_access_token)
|
|
106
|
+
curl -s -H "Authorization: Bearer $token" "${API_BASE}${path}"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
api_post() {
|
|
110
|
+
local path="$1"
|
|
111
|
+
local data="${2:-}"
|
|
112
|
+
local token
|
|
113
|
+
token=$(get_access_token)
|
|
114
|
+
if [[ -n "$data" ]]; then
|
|
115
|
+
curl -s -H "Authorization: Bearer $token" \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d "$data" "${API_BASE}${path}"
|
|
118
|
+
else
|
|
119
|
+
curl -s -X POST -H "Authorization: Bearer $token" "${API_BASE}${path}"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# --- Helpers ---
|
|
124
|
+
format_message_list() {
|
|
125
|
+
jq -r '
|
|
126
|
+
.messages[]? | "\(.id)\t\(.threadId)"
|
|
127
|
+
'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
format_message() {
|
|
131
|
+
local format="${1:-metadata}"
|
|
132
|
+
if [[ "$format" == "minimal" ]]; then
|
|
133
|
+
jq -r '{id, threadId, labelIds, snippet}'
|
|
134
|
+
else
|
|
135
|
+
jq -r '
|
|
136
|
+
def header(name): .payload.headers[]? | select(.name == name) | .value;
|
|
137
|
+
{
|
|
138
|
+
id: .id,
|
|
139
|
+
threadId: .threadId,
|
|
140
|
+
labels: .labelIds,
|
|
141
|
+
from: header("From"),
|
|
142
|
+
to: header("To"),
|
|
143
|
+
cc: (header("Cc") // null),
|
|
144
|
+
subject: header("Subject"),
|
|
145
|
+
date: header("Date"),
|
|
146
|
+
snippet: .snippet,
|
|
147
|
+
body: (
|
|
148
|
+
# Try to get text/plain body
|
|
149
|
+
if .payload.body.data then
|
|
150
|
+
.payload.body.data
|
|
151
|
+
elif .payload.parts then
|
|
152
|
+
(.payload.parts[]? | select(.mimeType == "text/plain") | .body.data) //
|
|
153
|
+
(.payload.parts[]? | select(.mimeType == "text/html") | .body.data)
|
|
154
|
+
else
|
|
155
|
+
null
|
|
156
|
+
end
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
'
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
decode_base64url() {
|
|
164
|
+
# Gmail uses URL-safe base64 — convert to standard base64 then decode
|
|
165
|
+
tr '_-' '/+' | base64 -d 2>/dev/null || echo "[decode error]"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# --- Commands ---
|
|
169
|
+
cmd_list() {
|
|
170
|
+
local max=10
|
|
171
|
+
while [[ $# -gt 0 ]]; do
|
|
172
|
+
case "$1" in
|
|
173
|
+
--max) max="$2"; shift 2 ;;
|
|
174
|
+
*) shift ;;
|
|
175
|
+
esac
|
|
176
|
+
done
|
|
177
|
+
|
|
178
|
+
local response
|
|
179
|
+
response=$(api_get "/messages?maxResults=$max&labelIds=INBOX")
|
|
180
|
+
|
|
181
|
+
# Get message IDs, then fetch metadata for each
|
|
182
|
+
local ids
|
|
183
|
+
ids=$(echo "$response" | jq -r '.messages[]?.id // empty')
|
|
184
|
+
|
|
185
|
+
if [[ -z "$ids" ]]; then
|
|
186
|
+
echo "No messages found."
|
|
187
|
+
return
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
echo "["
|
|
191
|
+
local first=true
|
|
192
|
+
while IFS= read -r mid; do
|
|
193
|
+
[[ -z "$mid" ]] && continue
|
|
194
|
+
if [[ "$first" == "true" ]]; then
|
|
195
|
+
first=false
|
|
196
|
+
else
|
|
197
|
+
echo ","
|
|
198
|
+
fi
|
|
199
|
+
api_get "/messages/$mid?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date" | jq '{
|
|
200
|
+
id: .id,
|
|
201
|
+
threadId: .threadId,
|
|
202
|
+
labels: .labelIds,
|
|
203
|
+
snippet: .snippet,
|
|
204
|
+
from: (.payload.headers[]? | select(.name == "From") | .value),
|
|
205
|
+
subject: (.payload.headers[]? | select(.name == "Subject") | .value),
|
|
206
|
+
date: (.payload.headers[]? | select(.name == "Date") | .value)
|
|
207
|
+
}'
|
|
208
|
+
done <<< "$ids"
|
|
209
|
+
echo "]"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
cmd_search() {
|
|
213
|
+
local query="$1"; shift
|
|
214
|
+
local max=10
|
|
215
|
+
while [[ $# -gt 0 ]]; do
|
|
216
|
+
case "$1" in
|
|
217
|
+
--max) max="$2"; shift 2 ;;
|
|
218
|
+
*) shift ;;
|
|
219
|
+
esac
|
|
220
|
+
done
|
|
221
|
+
|
|
222
|
+
local encoded_query
|
|
223
|
+
encoded_query=$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))" <<< "$query" 2>/dev/null || echo "$query")
|
|
224
|
+
|
|
225
|
+
local response
|
|
226
|
+
response=$(api_get "/messages?maxResults=$max&q=$encoded_query")
|
|
227
|
+
|
|
228
|
+
local ids
|
|
229
|
+
ids=$(echo "$response" | jq -r '.messages[]?.id // empty')
|
|
230
|
+
|
|
231
|
+
if [[ -z "$ids" ]]; then
|
|
232
|
+
echo "No messages found for query: $query"
|
|
233
|
+
return
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
echo "["
|
|
237
|
+
local first=true
|
|
238
|
+
while IFS= read -r mid; do
|
|
239
|
+
[[ -z "$mid" ]] && continue
|
|
240
|
+
if [[ "$first" == "true" ]]; then
|
|
241
|
+
first=false
|
|
242
|
+
else
|
|
243
|
+
echo ","
|
|
244
|
+
fi
|
|
245
|
+
api_get "/messages/$mid?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date" | jq '{
|
|
246
|
+
id: .id,
|
|
247
|
+
threadId: .threadId,
|
|
248
|
+
labels: .labelIds,
|
|
249
|
+
snippet: .snippet,
|
|
250
|
+
from: (.payload.headers[]? | select(.name == "From") | .value),
|
|
251
|
+
subject: (.payload.headers[]? | select(.name == "Subject") | .value),
|
|
252
|
+
date: (.payload.headers[]? | select(.name == "Date") | .value)
|
|
253
|
+
}'
|
|
254
|
+
done <<< "$ids"
|
|
255
|
+
echo "]"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
cmd_read() {
|
|
259
|
+
local msg_id="$1"; shift
|
|
260
|
+
local format="full"
|
|
261
|
+
while [[ $# -gt 0 ]]; do
|
|
262
|
+
case "$1" in
|
|
263
|
+
--format) format="$2"; shift 2 ;;
|
|
264
|
+
*) shift ;;
|
|
265
|
+
esac
|
|
266
|
+
done
|
|
267
|
+
|
|
268
|
+
local response
|
|
269
|
+
response=$(api_get "/messages/$msg_id?format=$format")
|
|
270
|
+
|
|
271
|
+
if [[ "$format" == "minimal" ]]; then
|
|
272
|
+
echo "$response" | format_message "minimal"
|
|
273
|
+
else
|
|
274
|
+
# Extract and decode body
|
|
275
|
+
local parsed
|
|
276
|
+
parsed=$(echo "$response" | format_message "full")
|
|
277
|
+
local body_b64
|
|
278
|
+
body_b64=$(echo "$parsed" | jq -r '.body // empty')
|
|
279
|
+
|
|
280
|
+
if [[ -n "$body_b64" ]]; then
|
|
281
|
+
local decoded
|
|
282
|
+
decoded=$(echo "$body_b64" | decode_base64url)
|
|
283
|
+
echo "$parsed" | jq --arg body "$decoded" '.body = $body'
|
|
284
|
+
else
|
|
285
|
+
echo "$parsed" | jq '.body = "[no text body found]"'
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
cmd_send() {
|
|
291
|
+
local to="" subject="" body="" cc="" bcc=""
|
|
292
|
+
while [[ $# -gt 0 ]]; do
|
|
293
|
+
case "$1" in
|
|
294
|
+
--to) to="$2"; shift 2 ;;
|
|
295
|
+
--subject) subject="$2"; shift 2 ;;
|
|
296
|
+
--body) body="$2"; shift 2 ;;
|
|
297
|
+
--cc) cc="$2"; shift 2 ;;
|
|
298
|
+
--bcc) bcc="$2"; shift 2 ;;
|
|
299
|
+
*) shift ;;
|
|
300
|
+
esac
|
|
301
|
+
done
|
|
302
|
+
|
|
303
|
+
if [[ -z "$to" || -z "$subject" ]]; then
|
|
304
|
+
echo "ERROR: --to and --subject are required" >&2
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# Build RFC 2822 message
|
|
309
|
+
local raw_msg="To: $to\nSubject: $subject\nContent-Type: text/plain; charset=utf-8\n"
|
|
310
|
+
[[ -n "$cc" ]] && raw_msg+="Cc: $cc\n"
|
|
311
|
+
[[ -n "$bcc" ]] && raw_msg+="Bcc: $bcc\n"
|
|
312
|
+
raw_msg+="\n$body"
|
|
313
|
+
|
|
314
|
+
# Base64url encode
|
|
315
|
+
local encoded
|
|
316
|
+
encoded=$(printf '%b' "$raw_msg" | base64 | tr '+/' '-_' | tr -d '=\n')
|
|
317
|
+
|
|
318
|
+
local data
|
|
319
|
+
data=$(jq -n --arg raw "$encoded" '{"raw": $raw}')
|
|
320
|
+
|
|
321
|
+
api_post "/messages/send" "$data" | jq '{id: .id, threadId: .threadId, labelIds: .labelIds}'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
cmd_reply() {
|
|
325
|
+
local msg_id="$1"; shift
|
|
326
|
+
local body=""
|
|
327
|
+
while [[ $# -gt 0 ]]; do
|
|
328
|
+
case "$1" in
|
|
329
|
+
--body) body="$2"; shift 2 ;;
|
|
330
|
+
*) shift ;;
|
|
331
|
+
esac
|
|
332
|
+
done
|
|
333
|
+
|
|
334
|
+
if [[ -z "$body" ]]; then
|
|
335
|
+
echo "ERROR: --body is required" >&2
|
|
336
|
+
exit 1
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Get original message to extract headers
|
|
340
|
+
local original
|
|
341
|
+
original=$(api_get "/messages/$msg_id?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Message-Id&metadataHeaders=To")
|
|
342
|
+
|
|
343
|
+
local from subject message_id thread_id
|
|
344
|
+
from=$(echo "$original" | jq -r '.payload.headers[]? | select(.name == "From") | .value')
|
|
345
|
+
subject=$(echo "$original" | jq -r '.payload.headers[]? | select(.name == "Subject") | .value')
|
|
346
|
+
message_id=$(echo "$original" | jq -r '.payload.headers[]? | select(.name == "Message-Id") | .value')
|
|
347
|
+
thread_id=$(echo "$original" | jq -r '.threadId')
|
|
348
|
+
|
|
349
|
+
# Add Re: prefix if not already present
|
|
350
|
+
[[ "$subject" != Re:* ]] && subject="Re: $subject"
|
|
351
|
+
|
|
352
|
+
local raw_msg="To: $from\nSubject: $subject\nIn-Reply-To: $message_id\nReferences: $message_id\nContent-Type: text/plain; charset=utf-8\n\n$body"
|
|
353
|
+
|
|
354
|
+
local encoded
|
|
355
|
+
encoded=$(printf '%b' "$raw_msg" | base64 | tr '+/' '-_' | tr -d '=\n')
|
|
356
|
+
|
|
357
|
+
local data
|
|
358
|
+
data=$(jq -n --arg raw "$encoded" --arg tid "$thread_id" '{"raw": $raw, "threadId": $tid}')
|
|
359
|
+
|
|
360
|
+
api_post "/messages/send" "$data" | jq '{id: .id, threadId: .threadId}'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
cmd_labels() {
|
|
364
|
+
api_get "/labels" | jq '[.labels[]? | {id: .id, name: .name, type: .type}]'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
cmd_label() {
|
|
368
|
+
local msg_id="$1"; shift
|
|
369
|
+
local add_labels=() remove_labels=()
|
|
370
|
+
while [[ $# -gt 0 ]]; do
|
|
371
|
+
case "$1" in
|
|
372
|
+
--add) add_labels+=("$2"); shift 2 ;;
|
|
373
|
+
--remove) remove_labels+=("$2"); shift 2 ;;
|
|
374
|
+
*) shift ;;
|
|
375
|
+
esac
|
|
376
|
+
done
|
|
377
|
+
|
|
378
|
+
local data
|
|
379
|
+
data=$(jq -n \
|
|
380
|
+
--argjson add "$(printf '%s\n' "${add_labels[@]}" | jq -R . | jq -s .)" \
|
|
381
|
+
--argjson remove "$(printf '%s\n' "${remove_labels[@]}" | jq -R . | jq -s .)" \
|
|
382
|
+
'{addLabelIds: $add, removeLabelIds: $remove}')
|
|
383
|
+
|
|
384
|
+
api_post "/messages/$msg_id/modify" "$data" | jq '{id: .id, labelIds: .labelIds}'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
cmd_mark_read() {
|
|
388
|
+
local msg_id="$1"
|
|
389
|
+
api_post "/messages/$msg_id/modify" '{"removeLabelIds": ["UNREAD"]}' | jq '{id: .id, labelIds: .labelIds}'
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
cmd_mark_unread() {
|
|
393
|
+
local msg_id="$1"
|
|
394
|
+
api_post "/messages/$msg_id/modify" '{"addLabelIds": ["UNREAD"]}' | jq '{id: .id, labelIds: .labelIds}'
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
cmd_trash() {
|
|
398
|
+
local msg_id="$1"
|
|
399
|
+
api_post "/messages/$msg_id/trash" | jq '{id: .id, labelIds: .labelIds}'
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
cmd_threads() {
|
|
403
|
+
local max=10
|
|
404
|
+
while [[ $# -gt 0 ]]; do
|
|
405
|
+
case "$1" in
|
|
406
|
+
--max) max="$2"; shift 2 ;;
|
|
407
|
+
*) shift ;;
|
|
408
|
+
esac
|
|
409
|
+
done
|
|
410
|
+
api_get "/threads?maxResults=$max" | jq '[.threads[]? | {id: .id, snippet: .snippet, historyId: .historyId}]'
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
cmd_thread() {
|
|
414
|
+
local thread_id="$1"
|
|
415
|
+
api_get "/threads/$thread_id?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date" | jq '{
|
|
416
|
+
id: .id,
|
|
417
|
+
messages: [.messages[]? | {
|
|
418
|
+
id: .id,
|
|
419
|
+
labels: .labelIds,
|
|
420
|
+
snippet: .snippet,
|
|
421
|
+
from: (.payload.headers[]? | select(.name == "From") | .value),
|
|
422
|
+
subject: (.payload.headers[]? | select(.name == "Subject") | .value),
|
|
423
|
+
date: (.payload.headers[]? | select(.name == "Date") | .value)
|
|
424
|
+
}]
|
|
425
|
+
}'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# --- Dispatch ---
|
|
429
|
+
COMMAND="${1:-}"
|
|
430
|
+
shift || true
|
|
431
|
+
|
|
432
|
+
case "$COMMAND" in
|
|
433
|
+
list) cmd_list "$@" ;;
|
|
434
|
+
search) cmd_search "$@" ;;
|
|
435
|
+
read) cmd_read "$@" ;;
|
|
436
|
+
send) cmd_send "$@" ;;
|
|
437
|
+
reply) cmd_reply "$@" ;;
|
|
438
|
+
labels) cmd_labels ;;
|
|
439
|
+
label) cmd_label "$@" ;;
|
|
440
|
+
mark-read) cmd_mark_read "$@" ;;
|
|
441
|
+
mark-unread) cmd_mark_unread "$@" ;;
|
|
442
|
+
trash) cmd_trash "$@" ;;
|
|
443
|
+
threads) cmd_threads "$@" ;;
|
|
444
|
+
thread) cmd_thread "$@" ;;
|
|
445
|
+
*)
|
|
446
|
+
echo "Usage: gmail.sh <command> [options]" >&2
|
|
447
|
+
echo "" >&2
|
|
448
|
+
echo "Commands: list, search, read, send, reply, labels, label," >&2
|
|
449
|
+
echo " mark-read, mark-unread, trash, threads, thread" >&2
|
|
450
|
+
exit 1
|
|
451
|
+
;;
|
|
452
|
+
esac
|