gogcli-mcp-classroom 4.4.0 → 4.5.1

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/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-classroom",
5
5
  "display_name": "gogcli (Classroom)",
6
- "version": "4.4.0",
6
+ "version": "4.5.1",
7
7
  "description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -115,7 +115,7 @@
115
115
  },
116
116
  {
117
117
  "name": "gog_classroom_courses_delete",
118
- "description": "Delete a course"
118
+ "description": "Delete a course (asks the user to confirm)"
119
119
  },
120
120
  {
121
121
  "name": "gog_classroom_courses_archive",
@@ -135,7 +135,7 @@
135
135
  },
136
136
  {
137
137
  "name": "gog_classroom_students_add",
138
- "description": "Add a student to a course"
138
+ "description": "Add a student to a course (asks the user to confirm unless adding yourself)"
139
139
  },
140
140
  {
141
141
  "name": "gog_classroom_students_remove",
@@ -151,7 +151,7 @@
151
151
  },
152
152
  {
153
153
  "name": "gog_classroom_teachers_add",
154
- "description": "Add a teacher to a course"
154
+ "description": "Add a teacher to a course (asks the user to confirm)"
155
155
  },
156
156
  {
157
157
  "name": "gog_classroom_teachers_remove",
@@ -171,15 +171,15 @@
171
171
  },
172
172
  {
173
173
  "name": "gog_classroom_coursework_create",
174
- "description": "Create coursework (assignment/question)"
174
+ "description": "Create coursework (assignment/question) (asks the user to confirm unless an unscheduled DRAFT)"
175
175
  },
176
176
  {
177
177
  "name": "gog_classroom_coursework_update",
178
- "description": "Update existing coursework"
178
+ "description": "Update existing coursework (publishing asks the user to confirm)"
179
179
  },
180
180
  {
181
181
  "name": "gog_classroom_coursework_delete",
182
- "description": "Delete coursework"
182
+ "description": "Delete coursework (asks the user to confirm)"
183
183
  },
184
184
  {
185
185
  "name": "gog_classroom_submissions_list",
@@ -195,7 +195,7 @@
195
195
  },
196
196
  {
197
197
  "name": "gog_classroom_submissions_return",
198
- "description": "Return a graded submission"
198
+ "description": "Return a graded submission (asks the user to confirm)"
199
199
  },
200
200
  {
201
201
  "name": "gog_classroom_submissions_turn_in",
@@ -219,7 +219,7 @@
219
219
  },
220
220
  {
221
221
  "name": "gog_classroom_announcements_update",
222
- "description": "Update an announcement"
222
+ "description": "Update an announcement (publishing asks the user to confirm)"
223
223
  },
224
224
  {
225
225
  "name": "gog_classroom_announcements_delete",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-classroom",
3
- "version": "4.4.0",
3
+ "version": "4.5.1",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-classroom",
5
5
  "description": "Extended Google Classroom MCP server via gogcli — auth + full Classroom support",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -24,7 +24,7 @@
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@chrischall/mcp-utils": "^2.4.0",
27
+ "@chrischall/mcp-utils": "^2.6.0",
28
28
  "@modelcontextprotocol/server": "^2.0.0",
29
29
  "zod": "^4.6.1"
30
30
  },
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-classroom"
9
9
  },
10
- "version": "4.4.0",
10
+ "version": "4.5.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-classroom",
15
- "version": "4.4.0",
15
+ "version": "4.5.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -1,12 +1,44 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose, pos, readCourse, requireDispatchConfirmation, CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam } from '../../../gogcli-mcp/src/lib.js';
3
+ import {
4
+ accountParam,
5
+ runOrDiagnose,
6
+ pos,
7
+ readCourse,
8
+ readCoursework,
9
+ readClassroomWork,
10
+ requireDispatchConfirmation,
11
+ bodyPreview,
12
+ CONFIRM_FALLBACK_DESCRIPTION,
13
+ confirmTokenParam,
14
+ } from '../../../gogcli-mcp/src/lib.js';
4
15
  import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
5
16
 
6
17
  const courseState = z.enum(['ACTIVE', 'ARCHIVED', 'PROVISIONED', 'DECLINED', 'SUSPENDED']);
7
18
  const workState = z.enum(['PUBLISHED', 'DRAFT']);
8
19
  const workType = z.enum(['ASSIGNMENT', 'SHORT_ANSWER_QUESTION', 'MULTIPLE_CHOICE_QUESTION']);
9
20
 
21
+ // ============================================================================
22
+ // PUBLISHING THROUGH UPDATE is on the dispatch rail (SEC-3, fleet-audit #932).
23
+ // gog_classroom_announcements_create asks unless the state is DRAFT — the
24
+ // draft path is documented as the way through on a client that cannot be
25
+ // prompted — so create-as-DRAFT then update-to-PUBLISHED was the same two-step
26
+ // around a gate that closed for Gmail drafts. An update that publishes (state
27
+ // PUBLISHED, or a schedule: a scheduled item is a draft that publishes itself)
28
+ // reads the course and the item and asks, with the text or title the class
29
+ // will actually see; a text edit, a due date or a return to DRAFT does not.
30
+ // ============================================================================
31
+
32
+ /** When an update makes work visible to students, in the words the prompt uses; undefined when it does not. */
33
+ function publishesWhen(state: string | undefined, scheduled: string | undefined): string | undefined {
34
+ if (scheduled) return `at ${scheduled}`;
35
+ return state === 'PUBLISHED' ? 'immediately' : undefined;
36
+ }
37
+
38
+ const PUBLISH_DESCRIPTION = ' Publishing it (state PUBLISHED, or a scheduled time) reads the course and the item and asks the MCP host to '
39
+ + 'show the user a confirmation prompt with the class and what students will see; nothing is published unless they '
40
+ + 'accept. Other edits, and a return to DRAFT, need no confirmation.' + CONFIRM_FALLBACK_DESCRIPTION;
41
+
10
42
  // Fields shared by courses_create and courses_update. `name` is required on
11
43
  // create, optional on update — keep it out of this fragment so each tool can
12
44
  // declare its own rule.
@@ -74,13 +106,33 @@ export function registerExtraClassroomTools(server: McpServer): void {
74
106
  });
75
107
 
76
108
  server.registerTool('gog_classroom_courses_delete', {
77
- description: 'Delete a Google Classroom course.',
109
+ description: 'Delete a Google Classroom course — its coursework, submissions and grades go with it, for good. Reads '
110
+ + 'the course and asks the MCP host to show the user a confirmation prompt naming it first; nothing is deleted '
111
+ + 'unless they accept. gog_classroom_courses_archive is the reversible alternative.' + CONFIRM_FALLBACK_DESCRIPTION,
78
112
  annotations: { destructiveHint: true },
79
113
  inputSchema: z.object({
80
114
  courseId: z.string().describe('Course ID'),
81
115
  account: accountParam,
116
+ confirmToken: confirmTokenParam,
82
117
  }),
83
- }, async ({ courseId, account }) => {
118
+ }, async ({ courseId, account, confirmToken }, ctx) => {
119
+ const read = await readCourse(courseId, account, runOrDiagnose);
120
+ if (read.error) return read.error;
121
+ const view = { course: read.course, deletes: 'the course with all of its coursework, submissions and grades, for good' };
122
+ const confirmation = await requireDispatchConfirmation(ctx, {
123
+ action: 'classroom.course-delete',
124
+ message: 'Review and confirm PERMANENTLY deleting this course:',
125
+ confirmationLabel: 'Confirm that this course should be deleted now.',
126
+ details: view,
127
+ unsupportedNote: 'Archive it with gog_classroom_courses_archive instead, or ask the user to delete it from Classroom.',
128
+ fallback: {
129
+ tool: 'gog_classroom_courses_delete',
130
+ account,
131
+ confirmToken,
132
+ subject: () => ({ target: courseId, payload: view, preview: view }),
133
+ },
134
+ });
135
+ if (confirmation) return confirmation;
84
136
  return runOrDiagnose(['classroom', 'courses', 'delete', pos(courseId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
85
137
  });
86
138
 
@@ -107,17 +159,39 @@ export function registerExtraClassroomTools(server: McpServer): void {
107
159
  });
108
160
 
109
161
  server.registerTool('gog_classroom_students_add', {
110
- description: 'Add a student to a Google Classroom course.',
162
+ description: 'Add a student to a Google Classroom course. Adding someone else gives them the class stream and '
163
+ + 'materials, so this reads the course and asks the MCP host to show the user a confirmation prompt with the class '
164
+ + 'and the student first; enrolling yourself ("me") does not ask.' + CONFIRM_FALLBACK_DESCRIPTION,
111
165
  annotations: { destructiveHint: true },
112
166
  inputSchema: z.object({
113
167
  courseId: z.string().describe('Course ID'),
114
- userId: z.string().describe('Student user ID (or "me")'),
168
+ userId: z.string().describe('Student user ID or email (or "me")'),
115
169
  enrollmentCode: z.string().optional().describe('Enrollment code (required if adding self via code)'),
116
170
  account: accountParam,
171
+ confirmToken: confirmTokenParam,
117
172
  }),
118
- }, async ({ courseId, userId, enrollmentCode, account }) => {
173
+ }, async ({ courseId, userId, enrollmentCode, account, confirmToken }, ctx) => {
119
174
  const args: GogArg[] = ['classroom', 'students', 'add', pos(courseId), pos(userId)];
120
175
  if (enrollmentCode) args.push(`--enrollment-code=${enrollmentCode}`);
176
+ if (userId.toLowerCase() !== 'me') {
177
+ const read = await readCourse(courseId, account, runOrDiagnose);
178
+ if (read.error) return read.error;
179
+ const view = { course: read.course, student: userId };
180
+ const confirmation = await requireDispatchConfirmation(ctx, {
181
+ action: 'classroom.student-add',
182
+ message: 'Review and confirm adding this student to the class:',
183
+ confirmationLabel: 'Confirm that this person should be added to the class now.',
184
+ details: view,
185
+ unsupportedNote: 'Ask the user to add them from Classroom, or invite them with gog_classroom_invitations_create.',
186
+ fallback: {
187
+ tool: 'gog_classroom_students_add',
188
+ account,
189
+ confirmToken,
190
+ subject: () => ({ target: `${courseId}/${userId}`, payload: view, preview: view }),
191
+ },
192
+ });
193
+ if (confirmation) return confirmation;
194
+ }
121
195
  return runOrDiagnose(args, { account });
122
196
  });
123
197
 
@@ -134,14 +208,34 @@ export function registerExtraClassroomTools(server: McpServer): void {
134
208
  });
135
209
 
136
210
  server.registerTool('gog_classroom_teachers_add', {
137
- description: 'Add a teacher to a Google Classroom course.',
211
+ description: 'Add a teacher to a Google Classroom course. A co-teacher sees every student\'s work and grades, so '
212
+ + 'this reads the course and asks the MCP host to show the user a confirmation prompt with the class and the '
213
+ + 'teacher first; nothing changes unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
138
214
  annotations: { destructiveHint: true },
139
215
  inputSchema: z.object({
140
216
  courseId: z.string().describe('Course ID'),
141
- userId: z.string().describe('Teacher user ID'),
217
+ userId: z.string().describe('Teacher user ID or email'),
142
218
  account: accountParam,
219
+ confirmToken: confirmTokenParam,
143
220
  }),
144
- }, async ({ courseId, userId, account }) => {
221
+ }, async ({ courseId, userId, account, confirmToken }, ctx) => {
222
+ const read = await readCourse(courseId, account, runOrDiagnose);
223
+ if (read.error) return read.error;
224
+ const view = { course: read.course, teacher: userId, grants: 'teacher access: every student\'s work, grades and roster' };
225
+ const confirmation = await requireDispatchConfirmation(ctx, {
226
+ action: 'classroom.teacher-add',
227
+ message: 'Review and confirm giving this person teacher access to the class:',
228
+ confirmationLabel: 'Confirm that this person should become a teacher of the class now.',
229
+ details: view,
230
+ unsupportedNote: 'Ask the user to add them from Classroom, or invite them with gog_classroom_invitations_create.',
231
+ fallback: {
232
+ tool: 'gog_classroom_teachers_add',
233
+ account,
234
+ confirmToken,
235
+ subject: () => ({ target: `${courseId}/${userId}`, payload: view, preview: view }),
236
+ },
237
+ });
238
+ if (confirmation) return confirmation;
145
239
  return runOrDiagnose(['classroom', 'teachers', 'add', pos(courseId), pos(userId)], { account });
146
240
  });
147
241
 
@@ -158,15 +252,20 @@ export function registerExtraClassroomTools(server: McpServer): void {
158
252
  });
159
253
 
160
254
  server.registerTool('gog_classroom_coursework_create', {
161
- description: 'Create a new coursework item (assignment, question, etc.) in a course.',
255
+ description: 'Create a new coursework item (assignment, question, etc.) in a course. Unless state is DRAFT with no '
256
+ + 'scheduled time (which students cannot see; a scheduled draft publishes itself), the class is notified, so this '
257
+ + 'reads the course and asks the MCP host to show the user a confirmation prompt with the class, the title, the '
258
+ + 'text, the due date and when it publishes; nothing is posted unless they accept. To stage one without asking, '
259
+ + 'pass state DRAFT and no scheduled time.' + CONFIRM_FALLBACK_DESCRIPTION,
162
260
  annotations: { destructiveHint: true },
163
261
  inputSchema: z.object({
164
262
  courseId: z.string().describe('Course ID'),
165
263
  title: z.string().describe('Coursework title'),
166
264
  ...courseworkSharedFields,
167
265
  account: accountParam,
266
+ confirmToken: confirmTokenParam,
168
267
  }),
169
- }, async ({ courseId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
268
+ }, async ({ courseId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account, confirmToken }, ctx) => {
170
269
  const args: GogArg[] = ['classroom', 'coursework', 'create', pos(courseId), `--title=${title}`];
171
270
  if (description) args.push(`--description=${description}`);
172
271
  if (type) args.push(`--type=${type}`);
@@ -177,11 +276,42 @@ export function registerExtraClassroomTools(server: McpServer): void {
177
276
  if (dueTime) args.push(`--due-time=${dueTime}`);
178
277
  if (scheduled) args.push(`--scheduled=${scheduled}`);
179
278
  if (topic) args.push(`--topic=${topic}`);
279
+ // An unscheduled draft reaches nobody until a teacher publishes it; a
280
+ // scheduled one publishes itself.
281
+ if (state !== 'DRAFT' || scheduled) {
282
+ const read = await readCourse(courseId, account, runOrDiagnose);
283
+ if (read.error) return read.error;
284
+ const work = {
285
+ course: read.course,
286
+ title,
287
+ type: type ?? 'ASSIGNMENT',
288
+ publishes: scheduled ? `at ${scheduled}` : 'immediately',
289
+ due: due ?? ([dueDate, dueTime].filter(Boolean).join(' ') || undefined),
290
+ maxPoints,
291
+ };
292
+ const confirmation = await requireDispatchConfirmation(ctx, {
293
+ action: 'classroom.coursework-create',
294
+ message: 'Review and confirm posting this coursework to the class:',
295
+ confirmationLabel: 'Confirm that this coursework should be posted to the class.',
296
+ details: { ...work, descriptionPreview: bodyPreview(description) },
297
+ unsupportedNote: 'Create it with state DRAFT instead; the user can review and post it from Classroom.',
298
+ fallback: {
299
+ tool: 'gog_classroom_coursework_create',
300
+ account,
301
+ confirmToken,
302
+ subject: () => {
303
+ const view = { ...work, description };
304
+ return { target: courseId, payload: view, preview: view };
305
+ },
306
+ },
307
+ });
308
+ if (confirmation) return confirmation;
309
+ }
180
310
  return runOrDiagnose(args, { account });
181
311
  });
182
312
 
183
313
  server.registerTool('gog_classroom_coursework_update', {
184
- description: 'Update an existing coursework item.',
314
+ description: 'Update an existing coursework item.' + PUBLISH_DESCRIPTION,
185
315
  annotations: { destructiveHint: true },
186
316
  inputSchema: z.object({
187
317
  courseId: z.string().describe('Course ID'),
@@ -189,8 +319,9 @@ export function registerExtraClassroomTools(server: McpServer): void {
189
319
  title: z.string().optional().describe('New title'),
190
320
  ...courseworkSharedFields,
191
321
  account: accountParam,
322
+ confirmToken: confirmTokenParam,
192
323
  }),
193
- }, async ({ courseId, courseworkId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
324
+ }, async ({ courseId, courseworkId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account, confirmToken }, ctx) => {
194
325
  const args: GogArg[] = ['classroom', 'coursework', 'update', pos(courseId), pos(courseworkId)];
195
326
  if (title) args.push(`--title=${title}`);
196
327
  if (description) args.push(`--description=${description}`);
@@ -202,37 +333,134 @@ export function registerExtraClassroomTools(server: McpServer): void {
202
333
  if (dueTime) args.push(`--due-time=${dueTime}`);
203
334
  if (scheduled) args.push(`--scheduled=${scheduled}`);
204
335
  if (topic) args.push(`--topic=${topic}`);
336
+ const publishes = publishesWhen(state, scheduled);
337
+ if (publishes) {
338
+ const read = await readCourse(courseId, account, runOrDiagnose);
339
+ if (read.error) return read.error;
340
+ const current = await readClassroomWork('coursework', courseId, courseworkId, account, runOrDiagnose);
341
+ if (current.error) return current.error;
342
+ // What the class will see: the new title/description when this call
343
+ // replaces them, else what the draft says now. `||`, not `??`: an empty
344
+ // string is never sent (the argv guards above are truthy checks), so gog
345
+ // keeps the current value and the prompt must show that one.
346
+ const published = description || current.work.description;
347
+ const publishedTitle = title || current.work.title;
348
+ const view = {
349
+ course: read.course,
350
+ coursework: { id: courseworkId, title: publishedTitle, state: current.work.state },
351
+ publishes,
352
+ descriptionPreview: bodyPreview(published),
353
+ };
354
+ const confirmation = await requireDispatchConfirmation(ctx, {
355
+ action: 'classroom.coursework-publish',
356
+ message: 'Review and confirm publishing this Classroom coursework to students:',
357
+ confirmationLabel: 'Confirm that this coursework should be published to the class.',
358
+ details: view,
359
+ unsupportedNote: 'Leave it as a DRAFT; the user can review and publish it from Classroom.',
360
+ fallback: {
361
+ tool: 'gog_classroom_coursework_update',
362
+ account,
363
+ confirmToken,
364
+ subject: () => ({
365
+ target: `${courseId}/${courseworkId}`,
366
+ revision: current.work.updateTime,
367
+ payload: { course: read.course, courseworkId, title: publishedTitle, description: published, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic },
368
+ preview: view,
369
+ }),
370
+ },
371
+ });
372
+ if (confirmation) return confirmation;
373
+ }
205
374
  return runOrDiagnose(args, { account });
206
375
  });
207
376
 
208
377
  server.registerTool('gog_classroom_coursework_delete', {
209
- description: 'Delete a coursework item.',
378
+ description: 'Delete a coursework item — every student submission to it goes too, for good. Reads the course and '
379
+ + 'the coursework and asks the MCP host to show the user a confirmation prompt naming both first; nothing is '
380
+ + 'deleted unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
210
381
  annotations: { destructiveHint: true },
211
382
  inputSchema: z.object({
212
383
  courseId: z.string().describe('Course ID'),
213
384
  courseworkId: z.string().describe('Coursework ID'),
214
385
  account: accountParam,
386
+ confirmToken: confirmTokenParam,
215
387
  }),
216
- }, async ({ courseId, courseworkId, account }) => {
388
+ }, async ({ courseId, courseworkId, account, confirmToken }, ctx) => {
389
+ const course = await readCourse(courseId, account, runOrDiagnose);
390
+ if (course.error) return course.error;
391
+ const work = await readCoursework(courseId, courseworkId, account, runOrDiagnose);
392
+ if (work.error) return work.error;
393
+ const view = { course: course.course, coursework: work.coursework, deletes: 'the coursework and every student submission to it, for good' };
394
+ const confirmation = await requireDispatchConfirmation(ctx, {
395
+ action: 'classroom.coursework-delete',
396
+ message: 'Review and confirm PERMANENTLY deleting this coursework and its submissions:',
397
+ confirmationLabel: 'Confirm that this coursework should be deleted now.',
398
+ details: view,
399
+ unsupportedNote: 'Ask the user to delete it from Classroom.',
400
+ fallback: {
401
+ tool: 'gog_classroom_coursework_delete',
402
+ account,
403
+ confirmToken,
404
+ subject: () => ({ target: `${courseId}/${courseworkId}`, payload: view, preview: view }),
405
+ },
406
+ });
407
+ if (confirmation) return confirmation;
217
408
  return runOrDiagnose(['classroom', 'coursework', 'delete', pos(courseId), pos(courseworkId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
218
409
  });
219
410
 
220
411
  server.registerTool('gog_classroom_announcements_update', {
221
- description: 'Update an existing announcement.',
412
+ description: 'Update an existing announcement.' + PUBLISH_DESCRIPTION,
222
413
  annotations: { destructiveHint: true },
223
414
  inputSchema: z.object({
224
415
  courseId: z.string().describe('Course ID'),
225
416
  announcementId: z.string().describe('Announcement ID'),
226
417
  text: z.string().optional().describe('New text'),
227
- state: workState.optional().describe('State'),
228
- scheduled: z.string().optional().describe('Scheduled publish time'),
418
+ state: workState.optional().describe('State (PUBLISHED asks the user to confirm; DRAFT does not)'),
419
+ scheduled: z.string().optional().describe('Scheduled publish time (asks the user to confirm)'),
229
420
  account: accountParam,
421
+ confirmToken: confirmTokenParam,
230
422
  }),
231
- }, async ({ courseId, announcementId, text, state, scheduled, account }) => {
423
+ }, async ({ courseId, announcementId, text, state, scheduled, account, confirmToken }, ctx) => {
232
424
  const args: GogArg[] = ['classroom', 'announcements', 'update', pos(courseId), pos(announcementId)];
233
425
  if (text) args.push(`--text=${text}`);
234
426
  if (state) args.push(`--state=${state}`);
235
427
  if (scheduled) args.push(`--scheduled=${scheduled}`);
428
+ const publishes = publishesWhen(state, scheduled);
429
+ if (publishes) {
430
+ const read = await readCourse(courseId, account, runOrDiagnose);
431
+ if (read.error) return read.error;
432
+ const current = await readClassroomWork('announcements', courseId, announcementId, account, runOrDiagnose);
433
+ if (current.error) return current.error;
434
+ // The text the class will see: the new one when this call replaces it,
435
+ // else what the draft says now. `||`, not `??`: an empty --text is never
436
+ // sent, so gog keeps the current text.
437
+ const published = text || current.work.text;
438
+ const view = {
439
+ course: read.course,
440
+ announcement: { id: announcementId, state: current.work.state },
441
+ publishes,
442
+ textPreview: bodyPreview(published),
443
+ };
444
+ const confirmation = await requireDispatchConfirmation(ctx, {
445
+ action: 'classroom.announcement-publish',
446
+ message: 'Review and confirm publishing this Classroom announcement to the class:',
447
+ confirmationLabel: 'Confirm that this announcement should be published to the class.',
448
+ details: view,
449
+ unsupportedNote: 'Leave it as a DRAFT; the user can review and publish it from Classroom.',
450
+ fallback: {
451
+ tool: 'gog_classroom_announcements_update',
452
+ account,
453
+ confirmToken,
454
+ subject: () => ({
455
+ target: `${courseId}/${announcementId}`,
456
+ revision: current.work.updateTime,
457
+ payload: { course: read.course, announcementId, text: published, state, scheduled },
458
+ preview: view,
459
+ }),
460
+ },
461
+ });
462
+ if (confirmation) return confirmation;
463
+ }
236
464
  return runOrDiagnose(args, { account });
237
465
  });
238
466