@chalksurf/cli 0.3.3 → 0.3.4

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.
@@ -20,6 +20,27 @@ var ApiClientError = class extends Error {
20
20
  var normalizeBaseUrl = (baseUrl) => {
21
21
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
22
22
  };
23
+ var getSafeBaseUrlForError = (baseUrl) => {
24
+ const url = new URL(baseUrl);
25
+ url.username = "";
26
+ url.password = "";
27
+ url.search = "";
28
+ url.hash = "";
29
+ const safeBaseUrl = url.toString();
30
+ return safeBaseUrl.endsWith("/") ? safeBaseUrl : `${safeBaseUrl}/`;
31
+ };
32
+ var getSafeNetworkCause = ({ error, token }) => {
33
+ if (!(error instanceof Error)) {
34
+ return void 0;
35
+ }
36
+ const causeCode = typeof error.cause === "object" && error.cause !== null && "code" in error.cause && typeof error.cause.code === "string" && /^[A-Z0-9_]+$/.test(error.cause.code) ? error.cause.code : void 0;
37
+ const errorMessage = error.message.trim();
38
+ const combinedCause = [causeCode, errorMessage].filter(Boolean).join(": ");
39
+ if (combinedCause.length === 0 || combinedCause.length > 200 || /authorization|bearer|password|secret|token/iu.test(combinedCause) || token && combinedCause.includes(token)) {
40
+ return void 0;
41
+ }
42
+ return combinedCause;
43
+ };
23
44
  var getErrorMessage = (payload, status) => {
24
45
  return payload?.error?.message ?? `API request failed with status ${status}`;
25
46
  };
@@ -51,6 +72,7 @@ var createApiClient = ({
51
72
  organizationId
52
73
  }) => {
53
74
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
75
+ const safeBaseUrlForError = getSafeBaseUrlForError(normalizedBaseUrl);
54
76
  const createHeaders = ({ includeJsonContentType }) => {
55
77
  const headers = new Headers();
56
78
  if (token) {
@@ -64,11 +86,21 @@ var createApiClient = ({
64
86
  }
65
87
  return headers;
66
88
  };
89
+ const fetchApi = async (url, init) => {
90
+ try {
91
+ return await fetch(url, init);
92
+ } catch (error) {
93
+ const safeCause = getSafeNetworkCause({ error, token });
94
+ throw new ApiClientError(
95
+ `Could not reach ChalkSurf API at ${safeBaseUrlForError}.${safeCause ? ` Cause: ${safeCause}` : ""}`
96
+ );
97
+ }
98
+ };
67
99
  const query = async (procedureName, input) => {
68
100
  const url = new URL(`trpc/${procedureName}`, normalizedBaseUrl);
69
101
  url.searchParams.set("batch", "1");
70
102
  url.searchParams.set("input", JSON.stringify({ 0: input ?? null }));
71
- const response = await fetch(url.toString(), {
103
+ const response = await fetchApi(url.toString(), {
72
104
  method: "GET",
73
105
  headers: createHeaders({ includeJsonContentType: false })
74
106
  });
@@ -76,7 +108,7 @@ var createApiClient = ({
76
108
  };
77
109
  const mutation = async (procedureName, input) => {
78
110
  const isFormDataInput = input instanceof FormData;
79
- const response = await fetch(new URL(`trpc/${procedureName}`, normalizedBaseUrl).toString(), {
111
+ const response = await fetchApi(new URL(`trpc/${procedureName}`, normalizedBaseUrl).toString(), {
80
112
  method: "POST",
81
113
  headers: createHeaders({ includeJsonContentType: !isFormDataInput }),
82
114
  body: isFormDataInput ? input : JSON.stringify(input ?? {})
@@ -140,9 +172,6 @@ var createApiClient = ({
140
172
  agentUploadExerciseFigure: async (input) => {
141
173
  return await mutation("agentUploadExerciseFigure", input);
142
174
  },
143
- agentAttachExerciseFigures: async (input) => {
144
- return await mutation("agentAttachExerciseFigures", input);
145
- },
146
175
  agentUpdateExercise: async (input) => {
147
176
  return await mutation("agentUpdateExercise", input);
148
177
  },
@@ -182,8 +211,11 @@ var createApiClient = ({
182
211
  agentBulkUpdateSheets: async (input) => {
183
212
  return await mutation("agentBulkUpdateSheets", input);
184
213
  },
185
- agentListSheetSeries: async () => {
186
- return await query("agentListSheetSeries", {});
214
+ agentListSheets: async (input) => {
215
+ return await query("agentListSheets", input);
216
+ },
217
+ agentListSheetSeries: async (input = {}) => {
218
+ return await query("agentListSheetSeries", input);
187
219
  },
188
220
  agentSetSheetVisibility: async (input) => {
189
221
  return await mutation("agentSetSheetVisibility", input);
@@ -931,7 +963,6 @@ import { z as z2 } from "zod";
931
963
  var exerciseStatusOptions = Constants.public.Enums.exercise_status;
932
964
  var exerciseQualityIssueSeverities = Constants.public.Enums.exercise_quality_issue_severity;
933
965
  var exerciseQualityIssueStatuses = Constants.public.Enums.exercise_quality_issue_status;
934
- var figureAssetSourceKinds = Constants.public.Enums.figure_asset_source_kind;
935
966
  var exerciseFigureRowSourceKinds = Constants.public.Enums.exercise_figure_source_kind;
936
967
  var exerciseFigureRenditionPurposes = Constants.public.Enums.exercise_figure_rendition_purpose;
937
968
  var exerciseVersionTypes = Constants.public.Enums.exercise_version_type;
@@ -1440,12 +1471,6 @@ var agentWriteOperationRegistry = {
1440
1471
  upload_exercise_figure: {
1441
1472
  agentToolName: "upload_exercise_figure",
1442
1473
  receiptOperation: "upload_exercise_figure",
1443
- resourceType: "figure_asset",
1444
- sync: true
1445
- },
1446
- attach_exercise_figures: {
1447
- agentToolName: "attach_exercise_figures",
1448
- receiptOperation: "attach_exercise_figures",
1449
1474
  resourceType: "exercise",
1450
1475
  sync: true
1451
1476
  },
@@ -1596,15 +1621,17 @@ var exerciseTranslationSchema = strictObject({
1596
1621
  status: z3.enum(exerciseStatusOptions)
1597
1622
  });
1598
1623
  var exerciseFigureWidthSchema = z3.number().finite().min(minExerciseFigureWidthInCm).max(maxExerciseFigureWidthInCm);
1599
- var agentLegacyExerciseFigureSchema = strictObject({
1624
+ var agentExerciseFigureInputSchema = strictObject({
1600
1625
  type: z3.enum(exerciseFigureTypes),
1601
1626
  url: z3.url(),
1602
1627
  widthInCm: exerciseFigureWidthSchema,
1603
1628
  exerciseFigureId: uuidSchema.optional(),
1604
- figureAssetId: uuidSchema.optional(),
1629
+ orderIndex: z3.number().int().min(0).optional(),
1630
+ rotationClockwiseDegrees: exerciseFigureRotationSchema.optional(),
1605
1631
  provenance: exerciseFigureProvenanceSchema.optional()
1606
1632
  });
1607
- var agentExerciseFigureSchema = agentLegacyExerciseFigureSchema.extend({
1633
+ var agentExerciseFigureSchema = agentExerciseFigureInputSchema.extend({
1634
+ exerciseFigureId: uuidSchema,
1608
1635
  orderIndex: z3.number().int().min(0),
1609
1636
  rotationClockwiseDegrees: exerciseFigureRotationSchema
1610
1637
  });
@@ -1703,6 +1730,34 @@ var folderSchema = strictObject({
1703
1730
  parentId: uuidSchema.nullable(),
1704
1731
  updatedAt: dateStringSchema
1705
1732
  });
1733
+ var folderPathSegmentSchema = strictObject({
1734
+ id: uuidSchema,
1735
+ name: z3.string().trim().min(1)
1736
+ });
1737
+ var listSheetsAgentInputSchema = strictObject({
1738
+ folderId: uuidSchema.optional(),
1739
+ includeDescendants: z3.boolean().optional().default(false),
1740
+ limit: z3.number().int().min(1).max(100).optional().default(50),
1741
+ offset: z3.number().int().min(0).optional().default(0)
1742
+ }).refine(({ folderId, includeDescendants }) => folderId !== void 0 || !includeDescendants, {
1743
+ message: "folderId is required when includeDescendants is true",
1744
+ path: ["folderId"]
1745
+ });
1746
+ var listSheetsAgentOutputSchema = strictObject({
1747
+ sheets: z3.array(
1748
+ strictObject({
1749
+ id: uuidSchema,
1750
+ name: z3.string().trim().min(1),
1751
+ subject: z3.enum(exerciseSubjects),
1752
+ isPublic: z3.boolean(),
1753
+ folderId: uuidSchema.nullable(),
1754
+ folderPath: z3.array(folderPathSegmentSchema),
1755
+ updatedAt: dateStringSchema,
1756
+ seriesAssignments: sheetSeriesAssignmentsSchema
1757
+ })
1758
+ ),
1759
+ totalCount: z3.number().int().min(0)
1760
+ });
1706
1761
  var exerciseLabelSchema = strictObject({
1707
1762
  id: z3.string().trim().min(1),
1708
1763
  subject: z3.enum(exerciseSubjects),
@@ -1955,7 +2010,7 @@ var getExerciseUsageAgentOutputSchema = strictObject({
1955
2010
  });
1956
2011
  var agentWriteReceiptSchema = strictObject({
1957
2012
  resource: strictObject({
1958
- type: z3.enum(["exercise", "sheet", "folder", "figure_asset"]),
2013
+ type: z3.enum(["exercise", "sheet", "folder"]),
1959
2014
  id: uuidSchema
1960
2015
  }),
1961
2016
  operation: agentWriteReceiptOperationSchema,
@@ -2031,14 +2086,6 @@ var exerciseCreateFigureSchema = strictObject({
2031
2086
  widthInCm: z3.number().positive().finite().optional(),
2032
2087
  provenance: exerciseFigureProvenanceSchema.optional()
2033
2088
  });
2034
- var agentExerciseFigureInputSchema = strictObject({
2035
- type: z3.enum(exerciseFigureTypes),
2036
- url: z3.url(),
2037
- widthInCm: exerciseFigureWidthSchema.optional(),
2038
- exerciseFigureId: uuidSchema.optional(),
2039
- figureAssetId: uuidSchema.optional(),
2040
- provenance: exerciseFigureProvenanceSchema.optional()
2041
- });
2042
2089
  var figureReviewWarningSchema = strictObject({
2043
2090
  code: z3.string().trim().min(1),
2044
2091
  message: z3.string().trim().min(1),
@@ -2066,7 +2113,7 @@ var uploadExerciseFigureSourceSchema = z3.discriminatedUnion("kind", [
2066
2113
  })
2067
2114
  ]);
2068
2115
  var uploadExerciseFigureAgentInputSchema = strictObject({
2069
- exerciseId: uuidSchema.optional(),
2116
+ exerciseId: uuidSchema,
2070
2117
  figureType: z3.enum(exerciseFigureTypes),
2071
2118
  source: uploadExerciseFigureSourceSchema,
2072
2119
  widthInCm: exerciseFigureWidthSchema.optional().default(defaultExerciseFigureWidthInCm),
@@ -2083,53 +2130,10 @@ var figureUploadReviewSchema = strictObject({
2083
2130
  nextActions: z3.array(z3.string().trim().min(1))
2084
2131
  });
2085
2132
  var uploadExerciseFigureAgentOutputSchema = strictObject({
2086
- figure: z3.union([agentExerciseFigureSchema, agentLegacyExerciseFigureSchema]),
2133
+ figure: agentExerciseFigureSchema,
2087
2134
  review: figureUploadReviewSchema,
2088
2135
  receipt: agentWriteReceiptSchema
2089
2136
  });
2090
- var attachExerciseFiguresAgentInputSchema = strictObject({
2091
- exerciseId: uuidSchema,
2092
- expectedUpdatedAt: expectedUpdatedAtSchema,
2093
- targetSheetId: uuidSchema.nullable().optional(),
2094
- allowSharedExerciseUpdate: z3.boolean().optional().default(false),
2095
- mode: z3.enum(["append", "replace_type", "replace_all"]),
2096
- figureType: z3.enum(exerciseFigureTypes).optional(),
2097
- figures: z3.array(agentExerciseFigureInputSchema)
2098
- }).superRefine((input, ctx) => {
2099
- if (input.mode === "append" && input.figures.length === 0) {
2100
- ctx.addIssue({
2101
- code: z3.ZodIssueCode.custom,
2102
- message: "append mode requires at least one figure.",
2103
- path: ["figures"]
2104
- });
2105
- }
2106
- if (input.mode === "replace_type" && !input.figureType) {
2107
- ctx.addIssue({
2108
- code: z3.ZodIssueCode.custom,
2109
- message: "replace_type mode requires figureType.",
2110
- path: ["figureType"]
2111
- });
2112
- }
2113
- if (input.mode !== "replace_type" && input.figureType) {
2114
- ctx.addIssue({
2115
- code: z3.ZodIssueCode.custom,
2116
- message: "figureType is only supported for replace_type mode.",
2117
- path: ["figureType"]
2118
- });
2119
- }
2120
- if (input.mode === "replace_type" && input.figureType) {
2121
- input.figures.forEach((figure, index) => {
2122
- if (figure.type !== input.figureType) {
2123
- ctx.addIssue({
2124
- code: z3.ZodIssueCode.custom,
2125
- message: "Replacement figures must match figureType.",
2126
- path: ["figures", index, "type"]
2127
- });
2128
- }
2129
- });
2130
- }
2131
- });
2132
- var attachExerciseFiguresAgentOutputSchema = agentWriteReceiptOutputSchema;
2133
2137
  var exercisePatchMetadataSchema = strictObject({
2134
2138
  sourceUrl: patchNullableStringSchema,
2135
2139
  solutionUrl: patchNullableStringSchema
@@ -2363,12 +2367,15 @@ var mcpPublicErrorCodes = [
2363
2367
  "storage_upload_failed",
2364
2368
  "unsupported_image",
2365
2369
  "validation_failed",
2370
+ "organization_selection_required",
2371
+ "organization_access_denied",
2366
2372
  "internal_error"
2367
2373
  ];
2374
+ var mcpPublicErrorCodeSchema = z5.enum(mcpPublicErrorCodes);
2368
2375
  var mcpToolErrorOutputSchema = strictObject2({
2369
2376
  isError: z5.literal(true),
2370
2377
  error: strictObject2({
2371
- code: z5.enum(mcpPublicErrorCodes),
2378
+ code: mcpPublicErrorCodeSchema,
2372
2379
  message: z5.string().trim().min(1),
2373
2380
  agentErrorDetails: agentErrorDetailsSchema.optional()
2374
2381
  })
@@ -2416,7 +2423,7 @@ var userSchema = strictObject2({
2416
2423
  email: z5.string().email().nullable(),
2417
2424
  name: z5.string().nullable()
2418
2425
  });
2419
- var organizationSchema = strictObject2({
2426
+ var mcpOrganizationSchema = strictObject2({
2420
2427
  id: z5.uuid(),
2421
2428
  name: z5.string(),
2422
2429
  mcpPermission: z5.enum(mcpOAuthGrantPermissions).nullable().optional(),
@@ -2549,14 +2556,31 @@ var chalksurfMcpScopes = {
2549
2556
  var getAuthStatusInputSchema = strictObject2({});
2550
2557
  var getAuthStatusOutputSchema = strictObject2({
2551
2558
  authenticated: z5.literal(true),
2559
+ organizations: z5.array(mcpOrganizationSchema),
2552
2560
  selectedOrganizationId: z5.uuid().nullable(),
2553
2561
  user: userSchema
2554
2562
  });
2555
2563
  var listOrganizationsInputSchema = strictObject2({});
2556
2564
  var listOrganizationsOutputSchema = strictObject2({
2557
- organizations: z5.array(organizationSchema),
2565
+ organizations: z5.array(mcpOrganizationSchema),
2558
2566
  selectedOrganizationId: z5.uuid().nullable()
2559
2567
  });
2568
+ var mcpOrganizationRecoveryOutputSchema = strictObject2({
2569
+ isError: z5.literal(true),
2570
+ error: strictObject2({
2571
+ code: z5.enum(["organization_selection_required", "organization_access_denied"]),
2572
+ message: z5.string().trim().min(1)
2573
+ }),
2574
+ organizations: z5.array(
2575
+ strictObject2({
2576
+ id: z5.uuid(),
2577
+ name: z5.string(),
2578
+ permission: z5.enum(mcpOAuthGrantPermissions),
2579
+ role: z5.enum(["owner", "editor", "viewer"]),
2580
+ type: z5.enum(["personal", "team"])
2581
+ })
2582
+ )
2583
+ });
2560
2584
  var searchExercisesInputSchema = strictObject2({
2561
2585
  organizationId: organizationIdSchema,
2562
2586
  age: z5.number().int().nullish(),
@@ -2587,9 +2611,7 @@ var getExerciseUsageInputSchema = getExerciseUsageAgentInputSchema.extend({
2587
2611
  organizationId: organizationIdSchema
2588
2612
  });
2589
2613
  var getExerciseUsageOutputSchema = getExerciseUsageAgentOutputSchema;
2590
- var listExerciseLabelsInputSchema = listExerciseLabelsAgentInputSchema.extend({
2591
- organizationId: organizationIdSchema
2592
- });
2614
+ var listExerciseLabelsInputSchema = listExerciseLabelsAgentInputSchema;
2593
2615
  var listExerciseLabelsOutputSchema = listExerciseLabelsAgentOutputSchema;
2594
2616
  var copyExerciseInputSchema = copyExerciseAgentInputSchema.extend({
2595
2617
  organizationId: organizationIdSchema
@@ -2662,13 +2684,15 @@ var searchSheetsOutputSchema = strictObject2({
2662
2684
  ),
2663
2685
  totalCount: z5.number().int().min(0)
2664
2686
  });
2665
- var getSheetInputSchema = getSheetAgentInputSchema.extend({
2687
+ var listSheetsInputSchema = listSheetsAgentInputSchema.extend({
2666
2688
  organizationId: organizationIdSchema
2667
2689
  });
2668
- var getSheetOutputSchema = getSheetAgentOutputSchema;
2669
- var listSheetSeriesInputSchema = listSheetSeriesAgentInputSchema.extend({
2690
+ var listSheetsOutputSchema = listSheetsAgentOutputSchema;
2691
+ var getSheetInputSchema = getSheetAgentInputSchema.extend({
2670
2692
  organizationId: organizationIdSchema
2671
2693
  });
2694
+ var getSheetOutputSchema = getSheetAgentOutputSchema;
2695
+ var listSheetSeriesInputSchema = listSheetSeriesAgentInputSchema;
2672
2696
  var listSheetSeriesOutputSchema = listSheetSeriesAgentOutputSchema;
2673
2697
  var listSheetVersionsInputSchema = listSheetVersionsAgentInputSchema.extend({
2674
2698
  organizationId: organizationIdSchema
@@ -2738,9 +2762,7 @@ var renameFolderInputSchema = renameFolderAgentInputSchema.extend({
2738
2762
  organizationId: organizationIdSchema
2739
2763
  });
2740
2764
  var renameFolderOutputSchema = renameFolderAgentOutputSchema;
2741
- var validateLatexSnippetsInputSchema = validateLatexSnippetsAgentInputSchema.extend({
2742
- organizationId: organizationIdSchema
2743
- });
2765
+ var validateLatexSnippetsInputSchema = validateLatexSnippetsAgentInputSchema;
2744
2766
  var validateLatexSnippetsOutputSchema = validateLatexSnippetsAgentOutputSchema;
2745
2767
  var importSheetInputSchema = strictObject2({
2746
2768
  organizationId: organizationIdSchema,
@@ -3497,6 +3519,7 @@ var uploadExerciseFigureInputSchema = uploadExerciseFigureAgentInputSchema.exten
3497
3519
  });
3498
3520
  var uploadExerciseFigureMcpToolInputSchema = strictObject2({
3499
3521
  organizationId: organizationIdSchema,
3522
+ exerciseId: z5.uuid(),
3500
3523
  figureType: z5.enum(exerciseFigureTypes),
3501
3524
  ...chatGptFigureUploadFileParamInputShape,
3502
3525
  source: strictObject2({
@@ -3513,10 +3536,6 @@ var uploadExerciseFigureMcpToolInputSchema = strictObject2({
3513
3536
  allowNeedsRevision: z5.boolean().optional()
3514
3537
  }).passthrough();
3515
3538
  var uploadExerciseFigureOutputSchema = uploadExerciseFigureAgentOutputSchema;
3516
- var attachExerciseFiguresInputSchema = attachExerciseFiguresAgentInputSchema.extend({
3517
- organizationId: organizationIdSchema
3518
- });
3519
- var attachExerciseFiguresOutputSchema = attachExerciseFiguresAgentOutputSchema;
3520
3539
  var chalksurfMcpToolCapabilities = {
3521
3540
  get_auth_status: {
3522
3541
  name: "get_auth_status",
@@ -3672,6 +3691,16 @@ var chalksurfMcpToolCapabilities = {
3672
3691
  requiredPermission: "read",
3673
3692
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3674
3693
  },
3694
+ list_sheets: {
3695
+ name: "list_sheets",
3696
+ title: "List sheets",
3697
+ description: "Use this to list exercise sheets owned by a ChalkSurf organization and inspect their folder paths and series assignments.",
3698
+ inputSchema: listSheetsInputSchema,
3699
+ outputSchema: listSheetsOutputSchema,
3700
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3701
+ requiredPermission: "read",
3702
+ security: { type: "oauth2", scopes: [chalksurfMcpScopes.read] }
3703
+ },
3675
3704
  get_sheet: {
3676
3705
  name: "get_sheet",
3677
3706
  title: "Get sheet",
@@ -3950,7 +3979,7 @@ var chalksurfMcpToolCapabilities = {
3950
3979
  upload_exercise_figure: {
3951
3980
  name: "upload_exercise_figure",
3952
3981
  title: "Upload exercise figure",
3953
- description: "Use this to attach one reviewed image, MCP file, HTTPS image, base64 image, or rendered figure to an exercise by providing exerciseId, figureType, source, and width. Uploading without exerciseId is a temporary compatibility path only.",
3982
+ description: "Use this to attach one reviewed image, MCP file, HTTPS image, base64 image, or rendered figure to an exercise by providing exerciseId, figureType, source, and width.",
3954
3983
  inputSchema: uploadExerciseFigureInputSchema,
3955
3984
  mcpInputSchema: uploadExerciseFigureMcpToolInputSchema,
3956
3985
  outputSchema: uploadExerciseFigureOutputSchema,
@@ -3958,16 +3987,6 @@ var chalksurfMcpToolCapabilities = {
3958
3987
  requiredPermission: "write",
3959
3988
  security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] },
3960
3989
  fileParams: chatGptFigureUploadFileParamNames
3961
- },
3962
- attach_exercise_figures: {
3963
- name: "attach_exercise_figures",
3964
- title: "Attach exercise figures",
3965
- description: "Deprecated compatibility tool. Use upload_exercise_figure with exerciseId for new figures; use this only to attach already-uploaded compatibility URLs or replace existing figure lists.",
3966
- inputSchema: attachExerciseFiguresInputSchema,
3967
- outputSchema: attachExerciseFiguresOutputSchema,
3968
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
3969
- requiredPermission: "write",
3970
- security: { type: "oauth2", scopes: [chalksurfMcpScopes.write] }
3971
3990
  }
3972
3991
  };
3973
3992
 
@@ -8719,29 +8738,106 @@ ${formatOrganizationSummary(result.organization)}`,
8719
8738
  };
8720
8739
 
8721
8740
  // src/commands/exercise.ts
8722
- import { readFile as readFile3 } from "node:fs/promises";
8723
- import { basename as basename2, extname as extname2, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
8741
+ import { readFile as readFile4 } from "node:fs/promises";
8742
+ import { basename as basename2, extname as extname2, isAbsolute as isAbsolute2, resolve as resolve5 } from "node:path";
8724
8743
  import { z as z13 } from "zod";
8725
8744
 
8726
- // src/lib/agent-patch.ts
8727
- var formatZodIssues = (issues) => {
8728
- return issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
8745
+ // src/lib/json-input.ts
8746
+ import { readFile as readFile2, stat } from "node:fs/promises";
8747
+ import { resolve } from "node:path";
8748
+ var isInteractiveStdin2 = (stdin) => {
8749
+ return stdin.isTTY === true;
8729
8750
  };
8730
- var parseJsonObjectFlag = ({ label, value }) => {
8731
- if (value === void 0) {
8732
- throw new CliCommandError(`${label} is required.`, 2);
8751
+ var readTextFromStdin = async (stdin) => {
8752
+ const chunks = [];
8753
+ for await (const chunk of stdin) {
8754
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
8733
8755
  }
8756
+ return Buffer.concat(chunks).toString("utf8");
8757
+ };
8758
+ var parseJsonObjectText = ({
8759
+ invalidJsonMessage,
8760
+ nonObjectMessage,
8761
+ text
8762
+ }) => {
8734
8763
  let parsedValue;
8735
8764
  try {
8736
- parsedValue = JSON.parse(value);
8765
+ parsedValue = JSON.parse(text);
8737
8766
  } catch {
8738
- throw new CliCommandError(`${label} must be valid JSON.`, 2);
8767
+ throw new CliCommandError(invalidJsonMessage, 2);
8739
8768
  }
8740
8769
  if (typeof parsedValue !== "object" || parsedValue === null || Array.isArray(parsedValue)) {
8741
- throw new CliCommandError(`${label} must be a JSON object.`, 2);
8770
+ throw new CliCommandError(nonObjectMessage, 2);
8742
8771
  }
8743
8772
  return parsedValue;
8744
8773
  };
8774
+ var loadJsonObjectInput = async ({
8775
+ cwd,
8776
+ inputJson,
8777
+ inputPath,
8778
+ stdin
8779
+ }) => {
8780
+ if (inputPath !== void 0 && inputJson !== void 0) {
8781
+ throw new CliCommandError("Pass --input or --input-json, not both.", 2);
8782
+ }
8783
+ if (inputPath === void 0 && inputJson === void 0) {
8784
+ throw new CliCommandError("One of --input or --input-json is required.", 2);
8785
+ }
8786
+ if (inputJson !== void 0) {
8787
+ return parseJsonObjectText({
8788
+ invalidJsonMessage: "--input-json must be valid JSON.",
8789
+ nonObjectMessage: "--input-json must be a JSON object.",
8790
+ text: inputJson
8791
+ });
8792
+ }
8793
+ if (inputPath === "-") {
8794
+ if (isInteractiveStdin2(stdin)) {
8795
+ throw new CliCommandError('No JSON was piped on stdin. Pipe JSON into "--input -".', 2);
8796
+ }
8797
+ const inputText = await readTextFromStdin(stdin);
8798
+ if (inputText.trim().length === 0) {
8799
+ throw new CliCommandError('No JSON was piped on stdin. Pipe JSON into "--input -".', 2);
8800
+ }
8801
+ return parseJsonObjectText({
8802
+ invalidJsonMessage: "Standard input must contain valid JSON.",
8803
+ nonObjectMessage: "Standard input must contain a JSON object.",
8804
+ text: inputText
8805
+ });
8806
+ }
8807
+ const resolvedInputPath = resolve(cwd, inputPath);
8808
+ let inputStats;
8809
+ try {
8810
+ inputStats = await stat(resolvedInputPath);
8811
+ } catch (error) {
8812
+ if (error.code === "ENOENT") {
8813
+ throw new CliCommandError(`Input file "${inputPath}" does not exist.`, 2);
8814
+ }
8815
+ throw error;
8816
+ }
8817
+ if (!inputStats.isFile()) {
8818
+ throw new CliCommandError(`Input path "${inputPath}" is not a file.`, 2);
8819
+ }
8820
+ return parseJsonObjectText({
8821
+ invalidJsonMessage: `Input file "${inputPath}" must contain valid JSON.`,
8822
+ nonObjectMessage: `Input file "${inputPath}" must contain a JSON object.`,
8823
+ text: await readFile2(resolvedInputPath, "utf8")
8824
+ });
8825
+ };
8826
+
8827
+ // src/lib/agent-patch.ts
8828
+ var formatZodIssues = (issues) => {
8829
+ return issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
8830
+ };
8831
+ var parseJsonObjectFlag = ({ label, value }) => {
8832
+ if (value === void 0) {
8833
+ throw new CliCommandError(`${label} is required.`, 2);
8834
+ }
8835
+ return parseJsonObjectText({
8836
+ invalidJsonMessage: `${label} must be valid JSON.`,
8837
+ nonObjectMessage: `${label} must be a JSON object.`,
8838
+ text: value
8839
+ });
8840
+ };
8745
8841
  var parseUpdateExerciseAgentInput = (input) => {
8746
8842
  const parsedInput = updateExerciseAgentInputSchema.safeParse(input);
8747
8843
  if (!parsedInput.success) {
@@ -8865,8 +8961,8 @@ var toApiOwnership = (ownership) => {
8865
8961
 
8866
8962
  // src/lib/import-files.ts
8867
8963
  import { openAsBlob } from "node:fs";
8868
- import { stat } from "node:fs/promises";
8869
- import { extname, resolve } from "node:path";
8964
+ import { stat as stat2 } from "node:fs/promises";
8965
+ import { extname, resolve as resolve2 } from "node:path";
8870
8966
  var guessMimeType = (fileName) => {
8871
8967
  return mimeTypesByExtension[extname(fileName).toLowerCase()] ?? "application/octet-stream";
8872
8968
  };
@@ -8937,7 +9033,7 @@ var buildFileImportCommandSources = async ({
8937
9033
  continue;
8938
9034
  }
8939
9035
  try {
8940
- const pathStats = await stat(resolve(cwd, rawSource));
9036
+ const pathStats = await stat2(resolve2(cwd, rawSource));
8941
9037
  if (pathStats.isDirectory()) {
8942
9038
  if (relativePath) {
8943
9039
  throw new CliCommandError("--relative-path cannot be used with directory sources.", 2);
@@ -9065,8 +9161,8 @@ var formatCliImportTranslationJobs = (translationJobs) => {
9065
9161
  };
9066
9162
 
9067
9163
  // src/lib/manifest.ts
9068
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
9069
- import { resolve as resolve2 } from "node:path";
9164
+ import { readFile as readFile3, stat as stat3 } from "node:fs/promises";
9165
+ import { resolve as resolve3 } from "node:path";
9070
9166
  import { z as z12 } from "zod";
9071
9167
 
9072
9168
  // src/lib/translation-languages.ts
@@ -9230,10 +9326,10 @@ var assertUniqueSourceIds = (sources) => {
9230
9326
  seenSourceIds.add(source.sourceId);
9231
9327
  }
9232
9328
  };
9233
- var isInteractiveStdin2 = (stdin) => {
9329
+ var isInteractiveStdin3 = (stdin) => {
9234
9330
  return stdin.isTTY === true;
9235
9331
  };
9236
- var readTextFromStdin = async (stdin) => {
9332
+ var readTextFromStdin2 = async (stdin) => {
9237
9333
  let text = "";
9238
9334
  for await (const chunk of stdin) {
9239
9335
  text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
@@ -9400,15 +9496,15 @@ var loadManifest = async ({
9400
9496
  stdin
9401
9497
  }) => {
9402
9498
  if (manifestPath === "-") {
9403
- if (isInteractiveStdin2(stdin)) {
9499
+ if (isInteractiveStdin3(stdin)) {
9404
9500
  throw new CliCommandError('No manifest was piped on stdin. Pipe JSON into "--manifest -".', 2);
9405
9501
  }
9406
- return parseManifest(await readTextFromStdin(stdin));
9502
+ return parseManifest(await readTextFromStdin2(stdin));
9407
9503
  }
9408
- const resolvedManifestPath = resolve2(cwd, manifestPath);
9504
+ const resolvedManifestPath = resolve3(cwd, manifestPath);
9409
9505
  let fileStats;
9410
9506
  try {
9411
- fileStats = await stat2(resolvedManifestPath);
9507
+ fileStats = await stat3(resolvedManifestPath);
9412
9508
  } catch (error) {
9413
9509
  if (error.code === "ENOENT") {
9414
9510
  throw new CliCommandError(`Manifest file "${manifestPath}" does not exist.`, 2);
@@ -9418,7 +9514,7 @@ var loadManifest = async ({
9418
9514
  if (!fileStats.isFile()) {
9419
9515
  throw new CliCommandError(`Manifest path "${manifestPath}" is not a file.`, 2);
9420
9516
  }
9421
- return parseManifest(await readFile2(resolvedManifestPath, "utf8"));
9517
+ return parseManifest(await readFile3(resolvedManifestPath, "utf8"));
9422
9518
  };
9423
9519
  var loadExerciseImportManifest = async ({
9424
9520
  cwd,
@@ -9471,9 +9567,9 @@ var loadSheetImportManifest = async ({
9471
9567
 
9472
9568
  // src/lib/source-resolver.ts
9473
9569
  import { createWriteStream } from "node:fs";
9474
- import { mkdtemp, open, readdir, rm as rm2, stat as stat3 } from "node:fs/promises";
9570
+ import { mkdtemp, open, readdir, rm as rm2, stat as stat4 } from "node:fs/promises";
9475
9571
  import { tmpdir } from "node:os";
9476
- import { basename, isAbsolute, relative, resolve as resolve3 } from "node:path";
9572
+ import { basename, isAbsolute, relative, resolve as resolve4 } from "node:path";
9477
9573
  import { Readable } from "node:stream";
9478
9574
  import { pipeline } from "node:stream/promises";
9479
9575
  import pLimit from "p-limit";
@@ -9498,7 +9594,7 @@ var normalizeRelativePath = (relativePath) => {
9498
9594
  var ensureExistingFile = async (filePath, label) => {
9499
9595
  let fileStats;
9500
9596
  try {
9501
- fileStats = await stat3(filePath);
9597
+ fileStats = await stat4(filePath);
9502
9598
  } catch (error) {
9503
9599
  if (error.code === "ENOENT") {
9504
9600
  throw createSourceResolutionError(`${label} "${filePath}" does not exist.`);
@@ -9512,7 +9608,7 @@ var ensureExistingFile = async (filePath, label) => {
9512
9608
  var ensureExistingDirectory = async (directoryPath, label) => {
9513
9609
  let directoryStats;
9514
9610
  try {
9515
- directoryStats = await stat3(directoryPath);
9611
+ directoryStats = await stat4(directoryPath);
9516
9612
  } catch (error) {
9517
9613
  if (error.code === "ENOENT") {
9518
9614
  throw createSourceResolutionError(`${label} "${directoryPath}" does not exist.`);
@@ -9535,7 +9631,7 @@ var collectFilesRecursively = async (directoryPath) => {
9535
9631
  const sortedEntries = directoryEntries.sort((leftEntry, rightEntry) => leftEntry.name.localeCompare(rightEntry.name));
9536
9632
  const filePaths = [];
9537
9633
  for (const directoryEntry of sortedEntries) {
9538
- const entryPath = resolve3(directoryPath, directoryEntry.name);
9634
+ const entryPath = resolve4(directoryPath, directoryEntry.name);
9539
9635
  if (directoryEntry.isDirectory()) {
9540
9636
  filePaths.push(...await collectFilesRecursively(entryPath));
9541
9637
  continue;
@@ -9596,7 +9692,7 @@ var resolveLocalFileSource = async ({
9596
9692
  cwd,
9597
9693
  sourceInputIndex
9598
9694
  }) => {
9599
- const resolvedPath = resolve3(cwd, source.path);
9695
+ const resolvedPath = resolve4(cwd, source.path);
9600
9696
  await ensureExistingFile(resolvedPath, "Local source");
9601
9697
  const relativePath = normalizeRelativePath(source.relativePath ?? basename(resolvedPath));
9602
9698
  return [
@@ -9616,8 +9712,8 @@ var resolveDirectorySource = async ({
9616
9712
  cwd,
9617
9713
  sourceInputIndex
9618
9714
  }) => {
9619
- const resolvedDirectoryPath = resolve3(cwd, source.path);
9620
- const resolvedRelativeRoot = resolve3(cwd, source.relativeRoot ?? source.path);
9715
+ const resolvedDirectoryPath = resolve4(cwd, source.path);
9716
+ const resolvedRelativeRoot = resolve4(cwd, source.relativeRoot ?? source.path);
9621
9717
  await ensureExistingDirectory(resolvedDirectoryPath, "Directory source");
9622
9718
  await ensureExistingDirectory(resolvedRelativeRoot, "Directory relativeRoot");
9623
9719
  if (!isContainedWithin({ childPath: resolvedDirectoryPath, parentPath: resolvedRelativeRoot })) {
@@ -9656,8 +9752,8 @@ var resolveUrlSource = async ({
9656
9752
  throw createSourceResolutionError(`URL source "${source.url}" must use http or https.`);
9657
9753
  }
9658
9754
  const relativePath = normalizeRelativePath(source.relativePath ?? deriveRelativePathFromUrl(parsedUrl));
9659
- const downloadDirectoryPath = await mkdtemp(resolve3(tmpdir(), "chalksurf-cli-source-"));
9660
- const downloadedFilePath = resolve3(downloadDirectoryPath, basename(relativePath));
9755
+ const downloadDirectoryPath = await mkdtemp(resolve4(tmpdir(), "chalksurf-cli-source-"));
9756
+ const downloadedFilePath = resolve4(downloadDirectoryPath, basename(relativePath));
9661
9757
  try {
9662
9758
  const response = await fetch(source.url);
9663
9759
  if (!response.ok) {
@@ -9979,9 +10075,6 @@ var formatUploadExerciseFigureOutput = ({ figure, receipt, review }) => [
9979
10075
  `figure: ${figure.type} ${figure.url}`,
9980
10076
  `image: ${review.pixelWidth ?? "?"}x${review.pixelHeight ?? "?"} ${review.byteLength} bytes`
9981
10077
  ].join("\n");
9982
- var formatAttachExerciseFiguresOutput = ({ receipt }) => {
9983
- return formatAgentWriteReceiptOutput({ receipt });
9984
- };
9985
10078
  var getExerciseTranslationJobIds = ({ jobs }) => jobs.map((job) => job.jobId);
9986
10079
  var formatExerciseTranslationOutput = ({ jobs }) => {
9987
10080
  if (jobs.length === 0) {
@@ -9998,16 +10091,6 @@ var resolveVisibilityFlag = ({ privateFlag, publicFlag }) => {
9998
10091
  }
9999
10092
  return Boolean(publicFlag);
10000
10093
  };
10001
- var parseJsonFlagValue = ({ label, value }) => {
10002
- if (value === void 0) {
10003
- throw new CliCommandError(`${label} is required.`, 2);
10004
- }
10005
- try {
10006
- return JSON.parse(value);
10007
- } catch {
10008
- throw new CliCommandError(`${label} must be valid JSON.`, 2);
10009
- }
10010
- };
10011
10094
  var resolveExerciseFigureType = ({
10012
10095
  label,
10013
10096
  required,
@@ -10024,13 +10107,6 @@ var resolveExerciseFigureType = ({
10024
10107
  }
10025
10108
  return value;
10026
10109
  };
10027
- var resolveAttachFigureMode = (value) => {
10028
- const mode = value ?? "append";
10029
- if (mode !== "append" && mode !== "replace_type" && mode !== "replace_all") {
10030
- throw new CliCommandError("--mode must be one of: append, replace_type, replace_all.", 2);
10031
- }
10032
- return mode;
10033
- };
10034
10110
  var parseUploadExerciseFigureInput = (input) => {
10035
10111
  const parsedInput = uploadExerciseFigureAgentInputSchema.safeParse(input);
10036
10112
  if (!parsedInput.success) {
@@ -10038,22 +10114,7 @@ var parseUploadExerciseFigureInput = (input) => {
10038
10114
  }
10039
10115
  return parsedInput.data;
10040
10116
  };
10041
- var parseAttachExerciseFiguresInput = (input) => {
10042
- const parsedInput = attachExerciseFiguresAgentInputSchema.safeParse(input);
10043
- if (!parsedInput.success) {
10044
- throw new CliCommandError(`Invalid figure attach input: ${formatZodIssues(parsedInput.error.issues)}`, 2);
10045
- }
10046
- return parsedInput.data;
10047
- };
10048
- var parseFigureJsonList = (value) => {
10049
- const parsedValue = parseJsonFlagValue({ label: "--figure-json", value });
10050
- const figures = Array.isArray(parsedValue) ? parsedValue : [parsedValue];
10051
- if (!figures.every((figure) => typeof figure === "object" && figure !== null && !Array.isArray(figure))) {
10052
- throw new CliCommandError("--figure-json must be a figure object or an array of figure objects.", 2);
10053
- }
10054
- return figures;
10055
- };
10056
- var resolveLocalPath = ({ cwd, path }) => isAbsolute2(path) ? path : resolve4(cwd, path);
10117
+ var resolveLocalPath = ({ cwd, path }) => isAbsolute2(path) ? path : resolve5(cwd, path);
10057
10118
  var getMimeTypeFromFileName = (fileName) => mimeTypesByExtension[extname2(fileName).toLowerCase()] ?? "";
10058
10119
  var buildLocalFigureUploadFormData = async ({
10059
10120
  context,
@@ -10062,7 +10123,7 @@ var buildLocalFigureUploadFormData = async ({
10062
10123
  }) => {
10063
10124
  const resolvedSourcePath = resolveLocalPath({ cwd: context.cwd, path: source });
10064
10125
  const fileName = input.originalFileName ?? basename2(resolvedSourcePath);
10065
- const fileBuffer = await readFile3(resolvedSourcePath);
10126
+ const fileBuffer = await readFile4(resolvedSourcePath);
10066
10127
  const file = new File([new Uint8Array(fileBuffer)], fileName, { type: getMimeTypeFromFileName(fileName) });
10067
10128
  const formData = new FormData();
10068
10129
  formData.set("payload", JSON.stringify(input));
@@ -10442,11 +10503,14 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10442
10503
  }
10443
10504
  ).command(
10444
10505
  "figure <subcommand>",
10445
- "Exercise figure upload and attachment commands",
10506
+ "Exercise figure upload commands",
10446
10507
  (figureYargs) => figureYargs.command(
10447
- "upload [source]",
10448
- "Upload a reviewed figure image to exercise-image storage",
10449
- (uploadYargs) => uploadYargs.positional("source", {
10508
+ "upload <exerciseId> [source]",
10509
+ "Upload and attach a reviewed figure image to an exercise",
10510
+ (uploadYargs) => uploadYargs.positional("exerciseId", {
10511
+ type: "string",
10512
+ describe: "Exercise ID to attach the figure to"
10513
+ }).positional("source", {
10450
10514
  type: "string",
10451
10515
  describe: "Local image file path. Omit when using --input-json for HTTPS/base64/rendered sources."
10452
10516
  }).option("input-json", {
@@ -10467,15 +10531,21 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10467
10531
  default: false,
10468
10532
  describe: "Allow uploading a rendered figure whose quality status is needs_revision"
10469
10533
  }).example(
10470
- "chalksurf exercise figure upload ./triangle.png --figure-type text --json",
10534
+ "chalksurf exercise figure upload 00000000-0000-4000-8000-000000000001 ./triangle.png --figure-type text --json",
10471
10535
  "Upload a local figure image"
10472
10536
  ).example(
10473
- `chalksurf exercise figure upload --input-json '{"figureType":"text","source":{"kind":"https_url","url":"https://example.com/figure.png"}}' --json`,
10537
+ `chalksurf exercise figure upload 00000000-0000-4000-8000-000000000001 --input-json '{"exerciseId":"00000000-0000-4000-8000-000000000001","figureType":"text","source":{"kind":"https_url","url":"https://example.com/figure.png"}}' --json`,
10474
10538
  "Upload a figure from an HTTPS image URL"
10475
10539
  ),
10476
10540
  async (argv) => {
10477
10541
  const source = argv.source;
10542
+ const exerciseId = resolveRequestedUuid({
10543
+ fallbackValue: argv.exerciseId,
10544
+ label: "exerciseId",
10545
+ required: true
10546
+ });
10478
10547
  const input = argv.inputJson === void 0 ? parseUploadExerciseFigureInput({
10548
+ exerciseId,
10479
10549
  figureType: resolveExerciseFigureType({
10480
10550
  label: "--figure-type",
10481
10551
  required: true,
@@ -10485,9 +10555,10 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10485
10555
  ...argv.widthCm === void 0 ? {} : { widthInCm: argv.widthCm },
10486
10556
  ...argv.originalFileName === void 0 ? {} : { originalFileName: argv.originalFileName },
10487
10557
  allowNeedsRevision: argv.allowNeedsRevision ?? false
10488
- }) : parseUploadExerciseFigureInput(
10489
- parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson })
10490
- );
10558
+ }) : parseUploadExerciseFigureInput({
10559
+ ...parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson }),
10560
+ exerciseId
10561
+ });
10491
10562
  if (argv.inputJson === void 0 && !source) {
10492
10563
  throw new CliCommandError("source is required unless --input-json is provided.", 2);
10493
10564
  }
@@ -10527,91 +10598,6 @@ var registerExerciseCommands = (exerciseYargs, context) => {
10527
10598
  }
10528
10599
  );
10529
10600
  }
10530
- ).command(
10531
- "attach [exerciseId]",
10532
- "Attach one or more uploaded figures to an exercise",
10533
- (attachYargs) => attachYargs.positional("exerciseId", {
10534
- type: "string",
10535
- describe: "Exercise ID to update"
10536
- }).option("expected-updated-at", {
10537
- type: "string",
10538
- demandOption: true,
10539
- describe: "Expected current exercise updated_at ISO timestamp"
10540
- }).option("figure-json", {
10541
- type: "string",
10542
- demandOption: true,
10543
- describe: "Figure object or array of figure objects, usually from exercise figure upload output"
10544
- }).option("mode", {
10545
- type: "string",
10546
- default: "append",
10547
- choices: ["append", "replace_type", "replace_all"],
10548
- describe: "How to merge figures into the exercise figure collection"
10549
- }).option("figure-type", {
10550
- type: "string",
10551
- choices: [...exerciseFigureTypes],
10552
- describe: "Required for --mode replace_type"
10553
- }).option("target-sheet-id", {
10554
- type: "string",
10555
- describe: "Sheet ID whose variant workflow this update targets"
10556
- }).option("allow-shared-exercise-update", {
10557
- type: "boolean",
10558
- default: false,
10559
- describe: "Acknowledge updating an exercise used outside --target-sheet-id"
10560
- }).example(
10561
- `chalksurf exercise figure attach 00000000-0000-4000-8000-000000000001 --expected-updated-at 2026-01-01T00:00:00.000Z --figure-json '{"type":"text","url":"https://...","widthInCm":6}' --json`,
10562
- "Append one uploaded figure to an exercise"
10563
- ),
10564
- async (argv) => {
10565
- const exerciseId = resolveRequestedUuid({
10566
- fallbackValue: argv.exerciseId,
10567
- label: "exerciseId",
10568
- required: true
10569
- });
10570
- const targetSheetId = resolveRequestedUuid({
10571
- flagValue: argv.targetSheetId,
10572
- label: "--target-sheet-id"
10573
- });
10574
- const mode = resolveAttachFigureMode(argv.mode);
10575
- const figureType = resolveExerciseFigureType({
10576
- label: "--figure-type",
10577
- required: mode === "replace_type",
10578
- value: argv.figureType
10579
- });
10580
- const input = parseAttachExerciseFiguresInput({
10581
- exerciseId,
10582
- expectedUpdatedAt: argv.expectedUpdatedAt,
10583
- ...targetSheetId === void 0 ? {} : { targetSheetId },
10584
- allowSharedExerciseUpdate: argv.allowSharedExerciseUpdate ?? false,
10585
- mode,
10586
- ...figureType === void 0 ? {} : { figureType },
10587
- figures: parseFigureJsonList(argv.figureJson)
10588
- });
10589
- const { apiClient, organizationId } = await createResolvedApiClient({
10590
- context,
10591
- baseUrlFlagValue: argv.baseUrl,
10592
- organizationFlagValue: argv.organization,
10593
- profileName: argv.profile
10594
- });
10595
- let result;
10596
- try {
10597
- result = await apiClient.agentAttachExerciseFigures(input);
10598
- } catch (error) {
10599
- throw mapApiErrorToCliError(error);
10600
- }
10601
- context.output.print(
10602
- {
10603
- request: {
10604
- ...input,
10605
- organizationId: organizationId ?? null
10606
- },
10607
- receipt: result.receipt
10608
- },
10609
- (output) => formatAttachExerciseFiguresOutput({ receipt: output.receipt }),
10610
- {
10611
- command: "exercise figure attach"
10612
- }
10613
- );
10614
- }
10615
10601
  ).demandCommand(1).strict(),
10616
10602
  () => {
10617
10603
  }
@@ -11511,7 +11497,7 @@ var registerFeedbackCommands = (feedbackYargs, context) => {
11511
11497
 
11512
11498
  // src/commands/figure.ts
11513
11499
  import { writeFile as writeFile2 } from "node:fs/promises";
11514
- import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
11500
+ import { isAbsolute as isAbsolute3, resolve as resolve6 } from "node:path";
11515
11501
  var parseFigureRenderDraftInput = (value) => {
11516
11502
  const parsedInput = figureRenderDraftInputSchema.safeParse(parseJsonObjectFlag({ label: "--input-json", value }));
11517
11503
  if (!parsedInput.success) {
@@ -11519,7 +11505,7 @@ var parseFigureRenderDraftInput = (value) => {
11519
11505
  }
11520
11506
  return parsedInput.data;
11521
11507
  };
11522
- var resolveOutputPath = ({ cwd, outputPath }) => isAbsolute3(outputPath) ? outputPath : resolve5(cwd, outputPath);
11508
+ var resolveOutputPath = ({ cwd, outputPath }) => isAbsolute3(outputPath) ? outputPath : resolve6(cwd, outputPath);
11523
11509
  var writeFigurePng = async ({
11524
11510
  cwd,
11525
11511
  outputPath,
@@ -12074,6 +12060,33 @@ var normalizeReadinessFilters = ({
12074
12060
  }) ?? []
12075
12061
  };
12076
12062
  };
12063
+ var normalizeSheetSeriesPath = (value) => {
12064
+ const normalizedValue = normalizeOptionalText(value);
12065
+ if (normalizedValue === null) {
12066
+ return null;
12067
+ }
12068
+ const parsedValue = sheetSeriesPathSchema.safeParse(normalizedValue);
12069
+ if (!parsedValue.success) {
12070
+ throw new CliCommandError(
12071
+ '--series-path is unknown. Run "chalksurf sheet series list --json" to inspect valid paths.',
12072
+ 2
12073
+ );
12074
+ }
12075
+ return parsedValue.data;
12076
+ };
12077
+ var normalizeSheetSeriesYearFilter = ({ fromYear, toYear }) => {
12078
+ if (fromYear === void 0 && toYear === void 0) {
12079
+ return null;
12080
+ }
12081
+ if (fromYear !== void 0 && toYear !== void 0 && fromYear > toYear) {
12082
+ throw new CliCommandError("--from-year must be less than or equal to --to-year.", 2);
12083
+ }
12084
+ const parsedFilter = sheetSeriesYearFilterSchema.safeParse({ fromYear, toYear });
12085
+ if (!parsedFilter.success) {
12086
+ throw new CliCommandError("--from-year and --to-year must be four-digit years.", 2);
12087
+ }
12088
+ return parsedFilter.data;
12089
+ };
12077
12090
  var hasUniqueItems2 = (values) => new Set(values).size === values.length;
12078
12091
  var sheetImportComponentIdPattern3 = /^[a-zA-Z0-9_-]{1,80}$/;
12079
12092
  var isValidTargetFolderPath2 = (value) => {
@@ -12501,6 +12514,19 @@ var formatSheetSearchOutput = ({
12501
12514
  )
12502
12515
  ].join("\n");
12503
12516
  };
12517
+ var formatSheetListOutput = ({ sheets, totalCount }) => {
12518
+ if (sheets.length === 0) {
12519
+ return "No sheets found.";
12520
+ }
12521
+ return [
12522
+ `${sheets.length} of ${totalCount} sheet${totalCount === 1 ? "" : "s"} returned.`,
12523
+ ...sheets.map((sheet) => {
12524
+ const folderPath = sheet.folderPath.map(({ name }) => name).join(" / ") || "(root)";
12525
+ const assignments = sheet.seriesAssignments.map(({ editionYear, seriesPath }) => `${seriesPath}@${editionYear}`).join(", ") || "unassigned";
12526
+ return `${sheet.id} ${sheet.name} ${folderPath} ${assignments}`;
12527
+ })
12528
+ ].join("\n");
12529
+ };
12504
12530
  var formatSheetReadinessSummary = (readinessSummary) => {
12505
12531
  if (!readinessSummary.hasIssues) {
12506
12532
  return "OK";
@@ -12649,6 +12675,73 @@ var resolveVisibilityFlag2 = ({ privateFlag, publicFlag }) => {
12649
12675
  };
12650
12676
  var registerSheetCommands = (sheetYargs, context) => {
12651
12677
  return sheetYargs.command(
12678
+ "list",
12679
+ "List exercise sheets owned by the selected organization",
12680
+ (listYargs) => listYargs.option("folder-id", {
12681
+ type: "string",
12682
+ describe: "Only include sheets placed directly in this folder"
12683
+ }).option("include-descendants", {
12684
+ type: "boolean",
12685
+ default: false,
12686
+ describe: "Include sheets in descendant folders; requires --folder-id"
12687
+ }).option("limit", {
12688
+ type: "number",
12689
+ default: 50,
12690
+ describe: "Maximum number of sheets to return"
12691
+ }).option("offset", {
12692
+ type: "number",
12693
+ default: 0,
12694
+ describe: "Number of organization-owned sheets to skip"
12695
+ }).example("chalksurf --profile prod-codex sheet list --json", "List sheets owned by the selected organization").example(
12696
+ "chalksurf --profile prod-codex sheet list --folder-id 00000000-0000-4000-8000-000000000001 --include-descendants --json",
12697
+ "List a folder subtree"
12698
+ ),
12699
+ async (argv) => {
12700
+ const folderId = resolveRequestedUuid2({
12701
+ flagValue: argv.folderId,
12702
+ label: "--folder-id"
12703
+ });
12704
+ const includeDescendants = Boolean(argv.includeDescendants);
12705
+ if (includeDescendants && !folderId) {
12706
+ throw new CliCommandError("--folder-id is required with --include-descendants.", 2);
12707
+ }
12708
+ const limit = normalizeLimit(argv.limit);
12709
+ const offset = normalizeOffset(argv.offset);
12710
+ const { apiClient, organizationId } = await createResolvedApiClient({
12711
+ context,
12712
+ baseUrlFlagValue: argv.baseUrl,
12713
+ organizationFlagValue: argv.organization,
12714
+ profileName: argv.profile
12715
+ });
12716
+ let result;
12717
+ try {
12718
+ result = await apiClient.agentListSheets({
12719
+ folderId,
12720
+ includeDescendants,
12721
+ limit,
12722
+ offset
12723
+ });
12724
+ } catch (error) {
12725
+ throw mapApiErrorToCliError(error);
12726
+ }
12727
+ context.output.print(
12728
+ {
12729
+ request: {
12730
+ folderId: folderId ?? null,
12731
+ includeDescendants,
12732
+ limit,
12733
+ offset,
12734
+ organizationId: organizationId ?? null
12735
+ },
12736
+ ...result
12737
+ },
12738
+ (output) => formatSheetListOutput(output),
12739
+ {
12740
+ command: "sheet list"
12741
+ }
12742
+ );
12743
+ }
12744
+ ).command(
12652
12745
  "search",
12653
12746
  "Search exercise sheets visible to the selected organization",
12654
12747
  (searchYargs) => searchYargs.option("text", {
@@ -12661,6 +12754,15 @@ var registerSheetCommands = (sheetYargs, context) => {
12661
12754
  }).option("subject", {
12662
12755
  type: "string",
12663
12756
  describe: `Only include sheets in this subject (${exerciseSubjects.join(", ")})`
12757
+ }).option("series-path", {
12758
+ type: "string",
12759
+ describe: "Only include sheets with an assignment at or below this authoritative series path"
12760
+ }).option("from-year", {
12761
+ type: "number",
12762
+ describe: "Only include matching series assignments from this edition year"
12763
+ }).option("to-year", {
12764
+ type: "number",
12765
+ describe: "Only include matching series assignments through this edition year"
12664
12766
  }).option("limit", {
12665
12767
  type: "number",
12666
12768
  default: 20,
@@ -12696,6 +12798,11 @@ var registerSheetCommands = (sheetYargs, context) => {
12696
12798
  label: "--subject",
12697
12799
  value: argv.subject
12698
12800
  });
12801
+ const seriesPath = normalizeSheetSeriesPath(argv.seriesPath);
12802
+ const yearFilter = normalizeSheetSeriesYearFilter({
12803
+ fromYear: argv.fromYear,
12804
+ toYear: argv.toYear
12805
+ });
12699
12806
  const limit = normalizeLimit(argv.limit);
12700
12807
  const offset = normalizeOffset(argv.offset);
12701
12808
  const readinessFilters = normalizeReadinessFilters({
@@ -12716,6 +12823,8 @@ var registerSheetCommands = (sheetYargs, context) => {
12716
12823
  issueTypes: readinessFilters.issueTypes,
12717
12824
  text,
12718
12825
  subject,
12826
+ seriesPath,
12827
+ yearFilter,
12719
12828
  ownership: toApiOwnership(ownership),
12720
12829
  limit,
12721
12830
  offset
@@ -12728,6 +12837,8 @@ var registerSheetCommands = (sheetYargs, context) => {
12728
12837
  request: {
12729
12838
  text,
12730
12839
  subject,
12840
+ seriesPath,
12841
+ yearFilter,
12731
12842
  ownership,
12732
12843
  hasIssues: readinessFilters.hasIssues,
12733
12844
  issueTypes: readinessFilters.issueTypes,
@@ -12934,8 +13045,16 @@ var registerSheetCommands = (sheetYargs, context) => {
12934
13045
  (seriesYargs) => seriesYargs.command(
12935
13046
  "list",
12936
13047
  "List valid sheet-series paths",
12937
- (listYargs) => listYargs.example("chalksurf sheet series list --json", "Inspect valid paths before assigning sheets"),
13048
+ (listYargs) => listYargs.option("subject", {
13049
+ type: "string",
13050
+ describe: `Only include series paths valid for this subject (${exerciseSubjects.join(", ")})`
13051
+ }).example("chalksurf sheet series list --json", "Inspect valid paths before assigning sheets").example("chalksurf sheet series list --subject math --json", "Inspect mathematics series paths"),
12938
13052
  async (argv) => {
13053
+ const subject = normalizeChoiceValue({
13054
+ allowedValues: exerciseSubjects,
13055
+ label: "--subject",
13056
+ value: argv.subject
13057
+ });
12939
13058
  const { apiClient, organizationId } = await createResolvedApiClient({
12940
13059
  context,
12941
13060
  baseUrlFlagValue: argv.baseUrl,
@@ -12944,13 +13063,15 @@ var registerSheetCommands = (sheetYargs, context) => {
12944
13063
  });
12945
13064
  let result;
12946
13065
  try {
12947
- result = await apiClient.agentListSheetSeries();
13066
+ result = await apiClient.agentListSheetSeries({
13067
+ subject: subject ?? void 0
13068
+ });
12948
13069
  } catch (error) {
12949
13070
  throw mapApiErrorToCliError(error);
12950
13071
  }
12951
13072
  context.output.print(
12952
13073
  {
12953
- request: { organizationId: organizationId ?? null },
13074
+ request: { organizationId: organizationId ?? null, subject },
12954
13075
  series: result.series
12955
13076
  },
12956
13077
  (output) => formatSheetSeriesOutput({ series: output.series }),
@@ -13612,17 +13733,32 @@ var registerSheetCommands = (sheetYargs, context) => {
13612
13733
  ).command(
13613
13734
  "bulk-update",
13614
13735
  "Dry-run or apply up to 100 sheet patches",
13615
- (bulkUpdateYargs) => bulkUpdateYargs.option("input-json", {
13736
+ (bulkUpdateYargs) => bulkUpdateYargs.option("input", {
13616
13737
  type: "string",
13617
- demandOption: true,
13618
- describe: "JSON matching the bulk update contract: mode, updates, and confirmationDigest for apply mode"
13738
+ nargs: 1,
13739
+ describe: 'Read the bulk update JSON object from a UTF-8 file, or "-" for stdin'
13740
+ }).option("input-json", {
13741
+ type: "string",
13742
+ nargs: 1,
13743
+ describe: "Inline JSON matching the bulk update contract: mode, updates, and confirmationDigest for apply mode"
13619
13744
  }).example(
13745
+ "chalksurf sheet bulk-update --input batch-001.json --json",
13746
+ "Validate a reviewed bulk-update batch from a file"
13747
+ ).example(
13748
+ "cat batch-001.json | chalksurf sheet bulk-update --input - --json",
13749
+ "Validate a reviewed bulk-update batch from stdin"
13750
+ ).example(
13620
13751
  `chalksurf sheet bulk-update --input-json '{"mode":"dry_run","updates":[{"id":"00000000-0000-4000-8000-000000000001","expectedUpdatedAt":"2026-01-01T00:00:00.000Z","patch":{"seriesAssignments":[{"seriesPath":"international.imo","editionYear":2025}]}}]}' --json`,
13621
13752
  "Validate sheet-series assignments and obtain a confirmation digest"
13622
13753
  ),
13623
13754
  async (argv) => {
13624
13755
  const input = parseBulkUpdateSheetsAgentInput(
13625
- parseJsonObjectFlag({ label: "--input-json", value: argv.inputJson })
13756
+ await loadJsonObjectInput({
13757
+ cwd: context.cwd,
13758
+ inputJson: argv.inputJson,
13759
+ inputPath: argv.input,
13760
+ stdin: context.stdin
13761
+ })
13626
13762
  );
13627
13763
  const { apiClient, organizationId } = await createResolvedApiClient({
13628
13764
  context,
@@ -14255,7 +14391,7 @@ var parseCliWithCapturedOutput = async ({
14255
14391
  argv,
14256
14392
  stdout
14257
14393
  }) => {
14258
- await new Promise((resolve6, reject) => {
14394
+ await new Promise((resolve7, reject) => {
14259
14395
  cli.parse(argv, (error, _parsedArgv, output) => {
14260
14396
  if (output) {
14261
14397
  stdout.write(output);
@@ -14264,7 +14400,7 @@ var parseCliWithCapturedOutput = async ({
14264
14400
  reject(error);
14265
14401
  return;
14266
14402
  }
14267
- resolve6();
14403
+ resolve7();
14268
14404
  });
14269
14405
  });
14270
14406
  };
@@ -14295,7 +14431,7 @@ var runCli = async ({
14295
14431
  configPath,
14296
14432
  env = process.env,
14297
14433
  nowImpl = Date.now,
14298
- sleepImpl = (milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)),
14434
+ sleepImpl = (milliseconds) => new Promise((resolve7) => setTimeout(resolve7, milliseconds)),
14299
14435
  stdin = process.stdin,
14300
14436
  stdout = process.stdout,
14301
14437
  stderr = process.stderr