@k2b/cloud 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/package.json +6 -4
  2. package/src/_internal/define-app.ts +3 -1
  3. package/src/ai/app-skills.ts +1 -7
  4. package/src/ai/audio-tool.ts +2 -1
  5. package/src/ai/browser.ts +109 -0
  6. package/src/ai/capabilities.ts +6 -2
  7. package/src/ai/chart-block.ts +431 -81
  8. package/src/ai/chat/action-sentences.ts +13 -7
  9. package/src/ai/chat/blocks.tsx +11 -7
  10. package/src/ai/chat/builtin-tools.tsx +9 -7
  11. package/src/ai/chat/chart-block.tsx +218 -44
  12. package/src/ai/chat/explorer-block.tsx +276 -0
  13. package/src/ai/chat/file-tools.tsx +40 -1
  14. package/src/ai/chat/message-actions.tsx +6 -0
  15. package/src/ai/chat/message-utils.ts +7 -0
  16. package/src/ai/chat/messages.ts +16 -0
  17. package/src/ai/chat/presentation.tsx +13 -3
  18. package/src/ai/chat/reference-text.tsx +34 -0
  19. package/src/ai/chat/tool-groups.ts +1 -0
  20. package/src/ai/chat/tool-names.ts +14 -0
  21. package/src/ai/chat/turn-view.tsx +17 -7
  22. package/src/ai/chat/view-block.browser-harness.tsx +35 -0
  23. package/src/ai/chat/view-block.tsx +417 -0
  24. package/src/ai/chat/visual-tools.tsx +32 -53
  25. package/src/ai/client-tool-results.ts +9 -0
  26. package/src/ai/default-tool-contracts.ts +334 -91
  27. package/src/ai/default-tools.ts +87 -11
  28. package/src/ai/executor.ts +173 -31
  29. package/src/ai/file-error.ts +2 -0
  30. package/src/ai/file-tools.ts +27 -7
  31. package/src/ai/files-store.ts +60 -0
  32. package/src/ai/html-pdf-tool.ts +3 -2
  33. package/src/ai/http.ts +37 -12
  34. package/src/ai/index.ts +90 -0
  35. package/src/ai/local-tool-contracts.ts +244 -0
  36. package/src/ai/malformed-tool-call-repair.ts +144 -0
  37. package/src/ai/markdown-pdf-tool.ts +2 -1
  38. package/src/ai/migrate.ts +7 -0
  39. package/src/ai/openai-api-messages.ts +53 -0
  40. package/src/ai/openai-api-slots.ts +40 -0
  41. package/src/ai/openai-api.ts +383 -0
  42. package/src/ai/protocol.ts +6 -2
  43. package/src/ai/provider.ts +16 -4
  44. package/src/ai/request-context.ts +33 -0
  45. package/src/ai/routes.ts +96 -57
  46. package/src/ai/runtime.ts +48 -12
  47. package/src/ai/settings.ts +5 -0
  48. package/src/ai/skill-catalog.ts +20 -5
  49. package/src/ai/skill-format.ts +17 -7
  50. package/src/ai/skill-tool.ts +15 -5
  51. package/src/ai/skills-routes.ts +20 -6
  52. package/src/ai/skills.ts +26 -7
  53. package/src/ai/store.ts +25 -0
  54. package/src/ai/system-prompt.ts +69 -10
  55. package/src/ai/timeline.ts +3 -1
  56. package/src/ai/tools.ts +3 -0
  57. package/src/ai/types.ts +9 -0
  58. package/src/ai/view-block.ts +941 -0
  59. package/src/ai/vision-tool.ts +2 -1
  60. package/src/api/admin-identity.ts +6 -1
  61. package/src/api/app-approval-actions.ts +36 -0
  62. package/src/api/app-approval.ts +10 -2
  63. package/src/api/approvals-loader.ts +15 -0
  64. package/src/api/approvals.ts +120 -0
  65. package/src/api/index.ts +3 -0
  66. package/src/api/me.ts +12 -3
  67. package/src/approvals/Approvals.browser-harness.tsx +16 -0
  68. package/src/approvals/Approvals.tsx +621 -0
  69. package/src/approvals/ApprovalsNavStatus.tsx +20 -0
  70. package/src/approvals/messages.ts +95 -0
  71. package/src/approvals/ui.ts +8 -0
  72. package/src/browser/app-approval.ts +32 -0
  73. package/src/browser/choose-files.tsx +26 -13
  74. package/src/browser/commands.ts +13 -4
  75. package/src/browser/file-provider-error.ts +11 -0
  76. package/src/browser/file-providers.ts +3 -12
  77. package/src/browser/files.tsx +1 -2
  78. package/src/browser/live-connection.ts +2 -3
  79. package/src/browser/provider-list.ts +2 -1
  80. package/src/browser/save-files.tsx +13 -2
  81. package/src/cli/account.ts +21 -2
  82. package/src/cli/commands.ts +4 -2
  83. package/src/contracts/app-approval.ts +120 -0
  84. package/src/contracts/approval-paths.ts +4 -0
  85. package/src/contracts/approvals.ts +81 -0
  86. package/src/contracts/command-path.ts +17 -0
  87. package/src/contracts/commands.ts +5 -15
  88. package/src/contracts/shared.ts +5 -0
  89. package/src/events/live-protocol.ts +2 -1
  90. package/src/events/live-server-message.ts +40 -0
  91. package/src/events/testing.ts +88 -0
  92. package/src/server/index.ts +2 -0
  93. package/src/server/middleware/auth.ts +11 -1
  94. package/src/server/request-timeout.ts +22 -0
  95. package/src/server/services/access.ts +4 -3
  96. package/src/services/app-approval.ts +103 -15
  97. package/src/services/approvals/cursor.ts +15 -0
  98. package/src/services/approvals/device.ts +112 -0
  99. package/src/services/approvals/index.ts +27 -11
  100. package/src/services/approvals/list.ts +120 -0
  101. package/src/services/approvals/migrate.ts +9 -1
  102. package/src/services/identity/models-only-auth.ts +25 -0
  103. package/src/services/notifications/email-frame.ts +16 -3
  104. package/src/services/service-account-credentials.ts +22 -10
  105. package/src/services/settings/core-settings.ts +27 -0
  106. package/src/shared/ai-platform-prompt.ts +26 -1
  107. package/src/shared/email-html.ts +4 -0
  108. package/src/shared/index.ts +6 -1
  109. package/src/shared/markdown/extensions/code.ts +14 -0
  110. package/src/shared/markdown/index.ts +4 -4
  111. package/src/ssr/AppBadge.tsx +4 -4
  112. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  113. package/src/ssr/RailApps.island.tsx +2 -2
  114. package/src/ssr/app-badges.ts +50 -6
  115. package/src/ssr/page-trail.browser-harness.tsx +52 -3
  116. package/src/ssr/rail-navigation.ts +5 -3
  117. package/src/ssr/runtime.ts +6 -1
  118. package/src/styles/approvals.css +390 -0
  119. package/src/styles/global.css +5 -2
@@ -6,6 +6,7 @@ import {
6
6
  } from "@k2b/cloud/services/document-extraction";
7
7
  import { z } from "zod";
8
8
  import type { RequestActor } from "../server";
9
+ import { aiMissingChatFileError } from "./file-error";
9
10
  import {
10
11
  AI_PROJECT_FILE_MOUNT,
11
12
  AI_SKILL_FILE_MOUNT,
@@ -14,8 +15,9 @@ import {
14
15
  mountAiProjectFilePath,
15
16
  mountAiSkillFilePath,
16
17
  } from "./file-mount";
17
- import { aiFileStore, guessAiMediaType, isAiWorkingFilePath, normalizeAiFilePath } from "./files-store";
18
+ import { aiFileStore, assertAiModelWritablePath, guessAiMediaType, isAiWorkingFilePath, normalizeAiFilePath } from "./files-store";
18
19
  import { defineAiTool } from "./tools";
20
+ import { CLOUD_AI_VIEW_MAX_BYTES, CloudAiViewSummarySchema, cloudAiViewSummary, isAiViewFile, parseCloudAiView } from "./view-block";
19
21
 
20
22
  const FILE_READ_MAX_BYTES = 64 * 1024;
21
23
  const FILE_WRITE_MAX_BYTES = 100 * 1024;
@@ -234,7 +236,7 @@ export const createCloudAiReadFileTool = () => {
234
236
  length: input.length,
235
237
  })
236
238
  : null);
237
- if (!stored) throw new Error(`No such file: ${path}`);
239
+ if (!stored) throw aiMissingChatFileError(path, ctx.localToolsEnabled);
238
240
  const extractAsDocument = shouldExtractDocument(path, stored.mediaType);
239
241
  if (isTextMediaType(stored.mediaType) && !extractAsDocument) {
240
242
  if (input.offset > stored.size) throw new Error(`Offset ${input.offset} is past the end of ${path} (${stored.size} bytes).`);
@@ -271,7 +273,7 @@ export const createCloudAiReadFileTool = () => {
271
273
  ? await aiFileStore.readTurnFile({ turnId: ctx.turnId, path })
272
274
  : await aiFileStore.read({ conversationId: conversationId(ctx.conversationId, "read_file"), path });
273
275
  if (!fullFile) {
274
- throw new Error(snapshot ? `Attached file snapshot is unavailable: ${path}` : `No such file: ${path}`);
276
+ throw snapshot ? new Error(`Attached file snapshot is unavailable: ${path}`) : aiMissingChatFileError(path, ctx.localToolsEnabled);
275
277
  }
276
278
  try {
277
279
  extracted = await extractDocumentMarkdown({ bytes: fullFile.bytes, filename: path, signal: ctx.signal });
@@ -328,6 +330,7 @@ export const createCloudAiWriteFileTool = () =>
328
330
  const id = conversationId(ctx.conversationId, "write_file");
329
331
  const path = toolPath(input.path, "write");
330
332
  assertConversationFilePath(path, "written");
333
+ assertAiModelWritablePath(path);
331
334
  const existing = await aiFileStore.stat({ conversationId: id, path });
332
335
  if (existing?.origin === "user") throw new Error(`Cannot overwrite user-uploaded file ${path}. Choose another path.`);
333
336
  const bytes = new TextEncoder().encode(input.content);
@@ -356,13 +359,29 @@ export const CloudAiPresentInputSchema = z.object({
356
359
  "One sentence in the user's language that says what the file is and what it is for, without repeating its name. Shown next to the file in the chat's results.",
357
360
  ),
358
361
  });
359
- export const CloudAiPresentOutputSchema = z.object({ path: z.string(), size: z.number(), mediaType: z.string() });
362
+ export const CloudAiPresentOutputSchema = z.object({
363
+ path: z.string(),
364
+ size: z.number(),
365
+ mediaType: z.string(),
366
+ view: CloudAiViewSummarySchema.optional(),
367
+ });
368
+
369
+ /** A view file as it is now; invalid user edits leave an ordinary file. */
370
+ const readView = async (conversationId: string, path: string) => {
371
+ const file = await aiFileStore.read({ conversationId, path });
372
+ if (!file || !isAiViewFile(file) || file.bytes.byteLength > CLOUD_AI_VIEW_MAX_BYTES) return undefined;
373
+ try {
374
+ return parseCloudAiView(JSON.parse(new TextDecoder().decode(file.bytes)));
375
+ } catch {
376
+ return undefined;
377
+ }
378
+ };
360
379
 
361
380
  export const createCloudAiPresentTool = () =>
362
381
  defineAiTool({
363
382
  name: "present",
364
383
  description:
365
- "Present a conversation file to the user as an openable and downloadable chat attachment. The returned path is a conversation-local file identifier, not a website URL. Refer to the delivered attachment by title; do not turn its path into a Markdown link.",
384
+ "Present a conversation file to the user as an openable and downloadable chat attachment. The returned path is a conversation-local file identifier, not a website URL. Refer to the delivered attachment by title; do not turn its path into a Markdown link. A view file from cloud.view.* (/views/…view.json) shows inline as its chart, group, table, stats or details view, so present every view the person should see.",
366
385
  inputSchema: CloudAiPresentInputSchema,
367
386
  outputSchema: CloudAiPresentOutputSchema,
368
387
  approval: "never",
@@ -372,8 +391,9 @@ export const createCloudAiPresentTool = () =>
372
391
  const path = toolPath(input.path, "read");
373
392
  assertConversationFilePath(path, "presented");
374
393
  const stat = await aiFileStore.stat({ conversationId: id, path });
375
- if (!stat) throw new Error(`No such file: ${path}`);
376
- return { path: stat.path, size: stat.size, mediaType: stat.mediaType };
394
+ if (!stat) throw aiMissingChatFileError(path, ctx.localToolsEnabled);
395
+ const view = isAiViewFile(stat) && stat.size <= CLOUD_AI_VIEW_MAX_BYTES ? await readView(id, path) : undefined;
396
+ return { path: stat.path, size: stat.size, mediaType: stat.mediaType, ...(view ? { view: cloudAiViewSummary(view) } : {}) };
377
397
  });
378
398
 
379
399
  type MathToken = { kind: "number"; value: number } | { kind: "ident"; name: string } | { kind: "op"; op: string };
@@ -2,6 +2,14 @@ import { type SQL, sql } from "bun";
2
2
  import { stripImageMetadata } from "../services/image-metadata";
3
3
  import { AiFileVersionConflict, AiFileWriteError, aiFileContentVersion } from "./file-content-version";
4
4
  import { isAiImage } from "./file-media-type";
5
+ import {
6
+ CLOUD_AI_VIEW_MAX_BYTES,
7
+ CLOUD_AI_VIEW_SUFFIX,
8
+ type CloudAiViewSummary,
9
+ checkCloudAiView,
10
+ cloudAiViewSummary,
11
+ isAiViewPath,
12
+ } from "./view-block";
5
13
 
6
14
  export { guessAiMediaType } from "./file-media-type";
7
15
 
@@ -697,6 +705,7 @@ export async function createAiConversationArtifact(input: {
697
705
  const { aiConversations } = await import("./store");
698
706
  const conversation = await aiConversations.getConversation({ conversationId: input.conversationId, ownerUserId: input.ownerUserId });
699
707
  if (!conversation || conversation.archivedAt) throw new Error("Conversation access denied");
708
+ assertAiModelWritablePath(input.path);
700
709
  return aiFileStore.createToolArtifact(input);
701
710
  }
702
711
 
@@ -710,6 +719,57 @@ export async function writeAiConversationFile(input: {
710
719
  expectedVersion: string | null;
711
720
  }): Promise<AiFileStat> {
712
721
  if (normalizeAiFilePath(input.path) !== input.path) throw new Error("Invalid file path");
722
+ assertAiModelWritablePath(input.path);
713
723
  const current = await aiFileStore.stat(input);
714
724
  return aiFileStore.write({ ...input, origin: current?.origin ?? "assistant", allowUserOverwrite: current?.origin === "user" });
715
725
  }
726
+
727
+ export function assertAiModelWritablePath(path: string): void {
728
+ if (isAiViewPath(normalizeAiFilePath(path) ?? path))
729
+ throw new Error("The /views folder holds views that code runs create with cloud.view.*; choose another path.");
730
+ }
731
+ export class AiViewInvalidError extends Error {
732
+ constructor(readonly issues: string[]) {
733
+ super(`View rejected: ${issues.join("; ")}`);
734
+ this.name = "AiViewInvalidError";
735
+ }
736
+ }
737
+
738
+ /** Run-produced views use the same ownership, budget and retry rules as other chat artifacts. */
739
+ export async function createAiConversationView(input: {
740
+ conversationId: string;
741
+ ownerUserId: string;
742
+ turnId: string;
743
+ runId: string;
744
+ index: number;
745
+ view: unknown;
746
+ }): Promise<{ path: string } & CloudAiViewSummary> {
747
+ const { aiConversations } = await import("./store");
748
+ const conversation = await aiConversations.getConversation({ conversationId: input.conversationId, ownerUserId: input.ownerUserId });
749
+ if (!conversation || conversation.archivedAt) throw new Error("Conversation access denied");
750
+ const checked = checkCloudAiView(input.view);
751
+ if ("issues" in checked) throw new AiViewInvalidError(checked.issues);
752
+ const bytes = new TextEncoder().encode(JSON.stringify(checked.view));
753
+ if (bytes.byteLength > CLOUD_AI_VIEW_MAX_BYTES)
754
+ throw new AiViewInvalidError([`view: At most ${CLOUD_AI_VIEW_MAX_BYTES} bytes are allowed.`]);
755
+ const slug =
756
+ checked.view.title
757
+ .normalize("NFKD")
758
+ .replace(/\p{M}/gu, "")
759
+ .toLowerCase()
760
+ .replace(/[^a-z0-9]+/g, "-")
761
+ .replace(/^-+|-+$/g, "")
762
+ .slice(0, 48)
763
+ .replace(/-+$/g, "") || "view";
764
+ const key = `${input.turnId}:${input.runId}:${input.index}`;
765
+ const digest = new Bun.CryptoHasher("sha256").update(key).digest("hex").slice(0, 8);
766
+ const path = `/views/${slug}-${digest}${CLOUD_AI_VIEW_SUFFIX}`;
767
+ await aiFileStore.createToolArtifact({
768
+ conversationId: input.conversationId,
769
+ path,
770
+ bytes,
771
+ mediaType: "application/json",
772
+ producerCallKey: `view:${key}`,
773
+ });
774
+ return { path, ...cloudAiViewSummary(checked.view) };
775
+ }
@@ -8,6 +8,7 @@ import {
8
8
  type RenderHtmlToPdfResult,
9
9
  renderHtmlToPdfWithConfig,
10
10
  } from "../services/pdf";
11
+ import { aiMissingChatFileError } from "./file-error";
11
12
  import { aiProjectFilePathFromMount, aiSkillFilePathFromMount } from "./file-mount";
12
13
  import { type AiFileContent, type AiFileStat, aiFileStore, normalizeAiFilePath } from "./files-store";
13
14
  import { withAiPdfConversionSlot } from "./pdf-conversions";
@@ -122,7 +123,7 @@ export const createCloudAiHtmlToPdfTool = (dependencies: HtmlPdfToolDependencies
122
123
  for (const path of [sourcePath, cssPath, headerPath, footerPath, ...assetPaths]) {
123
124
  if (path === undefined) continue;
124
125
  const file = await stat({ conversationId, path });
125
- if (!file) throw new Error(`No such file: ${path}`);
126
+ if (!file) throw aiMissingChatFileError(path, ctx.localToolsEnabled);
126
127
  if (path === sourcePath && file.origin !== "assistant") {
127
128
  throw new Error(`Create an assistant-owned HTML file with write_file before converting ${sourcePath}.`);
128
129
  }
@@ -134,7 +135,7 @@ export const createCloudAiHtmlToPdfTool = (dependencies: HtmlPdfToolDependencies
134
135
 
135
136
  const load = async (path: string): Promise<AiFileContent> => {
136
137
  const file = await read({ conversationId, path });
137
- if (!file) throw new Error(`No such file: ${path}`);
138
+ if (!file) throw aiMissingChatFileError(path, ctx.localToolsEnabled);
138
139
  return file;
139
140
  };
140
141
  const text = async (path: string | undefined): Promise<string | undefined> => (path === undefined ? undefined : utf8(await load(path)));
package/src/ai/http.ts CHANGED
@@ -6,6 +6,7 @@ import { type AuthContext, err, fail, respond } from "../server";
6
6
  import { aiAttachmentMarker } from "./attachments";
7
7
  import { CODE_RUNTIME_TOOL_NAMES } from "./browser-code-contracts";
8
8
  import { AI_TURN_ATTACHMENT_MAX_ITEMS } from "./limits";
9
+ import { AI_LOCAL_TOOL_NAMES, AiLocalClientSchema, AiLocalContextSchema } from "./local-tool-contracts";
9
10
  import { AiQuotaError } from "./quotas";
10
11
  import { AiResourceMarkerSchema } from "./resource-markers";
11
12
  import { AI_SHORT_ID_PATTERN } from "./short-id";
@@ -120,7 +121,10 @@ export const AiUserContentPartSchema = z.union([
120
121
 
121
122
  export type AiTurnContentPart = z.infer<typeof AiUserContentPartSchema>;
122
123
 
123
- export const AiClientToolIdSchema = z.enum(["local_bash", ...CODE_RUNTIME_TOOL_NAMES] satisfies [AiClientToolId, ...AiClientToolId[]]);
124
+ export const AiClientToolIdSchema = z.enum(["local_bash", ...AI_LOCAL_TOOL_NAMES, ...CODE_RUNTIME_TOOL_NAMES] satisfies [
125
+ AiClientToolId,
126
+ ...AiClientToolId[],
127
+ ]);
124
128
 
125
129
  // Names use aiCapabilityToolName's provider grammar and 64-character limit. The 64-item cap matches allowedTools, with headroom
126
130
  // for retired and not-yet-known names; 64 names × 64 ASCII characters bounds advertised name text to 4 KiB.
@@ -157,12 +161,21 @@ export const AiTurnInputSchema = z
157
161
 
158
162
  export type AiTurnInput = z.infer<typeof AiTurnInputSchema>;
159
163
 
160
- export const AiSubmitConversationDraftInputSchema = z.object({
161
- queueId: z.uuid().optional(),
162
- draftRevision: z.number().int().min(1),
163
- modelProfileId: z.string().trim().min(1).optional(),
164
- clientToolIds: AiClientToolIdsSchema.optional(),
165
- });
164
+ export const AiSubmitConversationDraftInputSchema = z
165
+ .object({
166
+ queueId: z.uuid().optional(),
167
+ draftRevision: z.number().int().min(1),
168
+ modelProfileId: z.string().trim().min(1).optional(),
169
+ clientToolIds: AiClientToolIdsSchema.optional(),
170
+ /** Set by a local client such as cld; the web never sends it. */
171
+ client: AiLocalClientSchema.optional(),
172
+ /** AGENTS.md files and local Skills a local client found; only valid together with client. */
173
+ localContext: AiLocalContextSchema.optional(),
174
+ })
175
+ .refine((input) => !input.localContext || Boolean(input.client), {
176
+ message: "Local context requires a local client.",
177
+ path: ["localContext"],
178
+ });
166
179
 
167
180
  export const AiSteerInputSchema = z.object({
168
181
  message: z.string().trim().min(1).max(20000),
@@ -187,6 +200,13 @@ export const AiMessageRetryInputSchema = z
187
200
  .max(AI_TURN_ATTACHMENT_MAX_ITEMS * 2 + 1)
188
201
  .optional(),
189
202
  modelProfileId: z.string().trim().min(1).optional(),
203
+ clientToolIds: AiClientToolIdsSchema.optional(),
204
+ client: AiLocalClientSchema.optional(),
205
+ localContext: AiLocalContextSchema.omit({ selectedSkills: true }).optional(),
206
+ })
207
+ .refine((input) => !input.localContext || Boolean(input.client), {
208
+ message: "Local context requires a local client.",
209
+ path: ["localContext"],
190
210
  })
191
211
  .refine(
192
212
  (input) =>
@@ -272,13 +292,18 @@ export const toAiErrorResponse = (c: Context<AuthContext>, error: unknown) => {
272
292
  return respond(c, fail(err.internal(message)));
273
293
  };
274
294
 
275
- export const toAiActionFailureResponse = (c: Context<AuthContext>, result: { ok: false; status: 400 | 404 | 409; message: string }) => {
295
+ export const toAiActionFailureResponse = (
296
+ c: Context<AuthContext>,
297
+ result: { ok: false; status: 400 | 403 | 404 | 409; message: string },
298
+ ) => {
276
299
  const failure =
277
300
  result.status === 400
278
301
  ? err.badInput(result.message)
279
- : result.status === 404
280
- ? // err.notFound appends " not found" to its subject — the runtime message is already complete.
281
- { code: "NOT_FOUND" as const, message: result.message, status: 404 as const }
282
- : conflict(result.message);
302
+ : result.status === 403
303
+ ? err.forbidden(result.message)
304
+ : result.status === 404
305
+ ? // err.notFound appends " not found" to its subject — the runtime message is already complete.
306
+ { code: "NOT_FOUND" as const, message: result.message, status: 404 as const }
307
+ : conflict(result.message);
283
308
  return respond(c, fail(failure));
284
309
  };
package/src/ai/index.ts CHANGED
@@ -56,7 +56,9 @@ export { listAiCredentialProfileIds } from "./credentials";
56
56
  export {
57
57
  CLOUD_AI_CHART_MAX_SERIES,
58
58
  CLOUD_AI_CHART_MAX_VALUES,
59
+ CLOUD_AI_GROUP_MAX_CHARTS,
59
60
  CLOUD_AI_TEXT_EDITOR_MAX_CHARS,
61
+ CLOUD_AI_VIEW_MAX_MARKS,
60
62
  type CloudAiCardInput,
61
63
  CloudAiCardInputSchema,
62
64
  type CloudAiCardOutput,
@@ -65,10 +67,13 @@ export {
65
67
  CloudAiChartInputSchema,
66
68
  type CloudAiChartOutput,
67
69
  CloudAiChartOutputSchema,
70
+ type CloudAiDateFormat,
71
+ type CloudAiFormat,
68
72
  type CloudAiLocalBashInput,
69
73
  CloudAiLocalBashInputSchema,
70
74
  type CloudAiLocalBashOutput,
71
75
  CloudAiLocalBashOutputSchema,
76
+ type CloudAiNumberFormat,
72
77
  type CloudAiSurveyInput,
73
78
  CloudAiSurveyInputSchema,
74
79
  type CloudAiSurveyOutput,
@@ -77,6 +82,10 @@ export {
77
82
  CloudAiTextEditorInputSchema,
78
83
  type CloudAiTextEditorOutput,
79
84
  CloudAiTextEditorOutputSchema,
85
+ cloudAiAppChartSchema,
86
+ cloudAiChartSchema,
87
+ cloudAiFormatSchema,
88
+ cloudAiNumberFormatSchema,
80
89
  parseCloudAiChartInput,
81
90
  } from "./default-tool-contracts";
82
91
  export {
@@ -99,7 +108,9 @@ export {
99
108
  type AiConversationFileOverview,
100
109
  type AiConversationWorkingGroup,
101
110
  type AiFileStat,
111
+ AiViewInvalidError,
102
112
  createAiConversationArtifact,
113
+ createAiConversationView,
103
114
  guessAiMediaType,
104
115
  isAiWorkingFilePath,
105
116
  listAiConversationFiles,
@@ -154,6 +165,50 @@ export {
154
165
  AiInvalidationDomainSchema,
155
166
  AiInvalidationSchema,
156
167
  } from "./live-events";
168
+ export {
169
+ AI_LOCAL_BATCH_MAX_ITEMS,
170
+ AI_LOCAL_INSTRUCTIONS_MAX_BYTES,
171
+ AI_LOCAL_LIST_MAX_LIMIT,
172
+ AI_LOCAL_READ_MAX_BYTES,
173
+ AI_LOCAL_SELECTED_SKILLS_MAX_ITEMS,
174
+ AI_LOCAL_SKILL_FILES_MAX_ITEMS,
175
+ AI_LOCAL_SKILLS_MAX_ITEMS,
176
+ AI_LOCAL_TOOL_NAMES,
177
+ AI_LOCAL_WRITE_MAX_BYTES,
178
+ type AiLocalClient,
179
+ AiLocalClientSchema,
180
+ type AiLocalContext,
181
+ AiLocalContextSchema,
182
+ type AiLocalToolName,
183
+ type CloudAiLoadLocalSkillInput,
184
+ CloudAiLoadLocalSkillInputSchema,
185
+ type CloudAiLoadLocalSkillOutput,
186
+ CloudAiLoadLocalSkillOutputSchema,
187
+ type CloudAiLocalDownloadInput,
188
+ CloudAiLocalDownloadInputSchema,
189
+ type CloudAiLocalDownloadOutput,
190
+ CloudAiLocalDownloadOutputSchema,
191
+ type CloudAiLocalListInput,
192
+ CloudAiLocalListInputSchema,
193
+ type CloudAiLocalListOutput,
194
+ CloudAiLocalListOutputSchema,
195
+ type CloudAiLocalMoveInput,
196
+ CloudAiLocalMoveInputSchema,
197
+ type CloudAiLocalMoveOutput,
198
+ CloudAiLocalMoveOutputSchema,
199
+ type CloudAiLocalReadInput,
200
+ CloudAiLocalReadInputSchema,
201
+ type CloudAiLocalReadOutput,
202
+ CloudAiLocalReadOutputSchema,
203
+ type CloudAiLocalUploadInput,
204
+ CloudAiLocalUploadInputSchema,
205
+ type CloudAiLocalUploadOutput,
206
+ CloudAiLocalUploadOutputSchema,
207
+ type CloudAiLocalWriteInput,
208
+ CloudAiLocalWriteInputSchema,
209
+ type CloudAiLocalWriteOutput,
210
+ CloudAiLocalWriteOutputSchema,
211
+ } from "./local-tool-contracts";
157
212
  export { AI_ENRICH_CRON_SETTING_KEY, AI_MEMORY_LEARNING_CRON_SETTING_KEY, aiMaintenanceJobs } from "./maintenance";
158
213
  export {
159
214
  AI_MEMORY_CONTENT_MAX_CHARS,
@@ -257,6 +312,8 @@ export {
257
312
  type AiSkillDocument,
258
313
  type AiSkillExtraFrontmatter,
259
314
  type AiSkillReferenceInput,
315
+ isAiSkillManualOnly,
316
+ mountAiSkillInstructionLinks,
260
317
  parseAiSkillMarkdown,
261
318
  serializeAiSkillMarkdown,
262
319
  validateAiSkillDescription,
@@ -371,6 +428,39 @@ export type {
371
428
  } from "./types";
372
429
  export { AI_MESSAGE_FEEDBACK_REASONS, isAiImageMediaType } from "./types";
373
430
  export { isAiSettingsError } from "./validate";
431
+ export {
432
+ CLOUD_AI_STATS_MAX_ITEMS,
433
+ CLOUD_AI_TABLE_MAX_COLUMNS,
434
+ CLOUD_AI_TABLE_MAX_ROWS,
435
+ CLOUD_AI_VIEW_DIRECTORY,
436
+ CLOUD_AI_VIEW_MAX_BYTES,
437
+ CLOUD_AI_VIEW_SUFFIX,
438
+ type CloudAiChartView,
439
+ type CloudAiDetailsView,
440
+ type CloudAiExplorerRequest,
441
+ type CloudAiExplorerView,
442
+ type CloudAiGroupView,
443
+ type CloudAiStatsView,
444
+ type CloudAiTableView,
445
+ type CloudAiView,
446
+ type CloudAiViewReference,
447
+ type CloudAiViewSummary,
448
+ CloudAiViewSummarySchema,
449
+ type CloudAiViewTable,
450
+ checkCloudAiView,
451
+ cloudAiDetailsRows,
452
+ cloudAiExplorerControls,
453
+ cloudAiExplorerState,
454
+ cloudAiStatsTiles,
455
+ cloudAiTableTsv,
456
+ cloudAiViewSummary,
457
+ cloudAiViewTable,
458
+ isAiViewFile,
459
+ isAiViewPath,
460
+ parseCloudAiChartToolView,
461
+ parseCloudAiView,
462
+ sortCloudAiViewRows,
463
+ } from "./view-block";
374
464
  export {
375
465
  type CloudAiViewImageInput,
376
466
  CloudAiViewImageInputSchema,
@@ -0,0 +1,244 @@
1
+ import { z } from "zod";
2
+ import { AI_SKILL_DESCRIPTION_MAX_CHARS, AI_SKILL_INSTRUCTIONS_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
3
+
4
+ /**
5
+ * Contracts of the client tools a local client such as `cld` runs on the user's computer, and of the context it
6
+ * sends with a turn. Cloud declares and validates them; the client executes them and enforces its own path rules.
7
+ */
8
+ export const AI_LOCAL_TOOL_NAMES = [
9
+ "local_list",
10
+ "local_read",
11
+ "local_upload",
12
+ "local_download",
13
+ "local_write",
14
+ "local_move",
15
+ "load_local_skill",
16
+ ] as const;
17
+ export type AiLocalToolName = (typeof AI_LOCAL_TOOL_NAMES)[number];
18
+
19
+ /** Longest absolute path a client reports, Linux PATH_MAX. */
20
+ export const AI_LOCAL_PATH_MAX_CHARS = 4096;
21
+ /** Entries per local_list page by default and at most, like code_files. */
22
+ export const AI_LOCAL_LIST_DEFAULT_LIMIT = 100;
23
+ export const AI_LOCAL_LIST_MAX_LIMIT = 1000;
24
+ /** Bytes per local_read call by default and at most, like read_file. */
25
+ export const AI_LOCAL_READ_DEFAULT_BYTES = 16 * 1024;
26
+ export const AI_LOCAL_READ_MAX_BYTES = 64 * 1024;
27
+ /** Text one local_write call writes, like write_file. */
28
+ export const AI_LOCAL_WRITE_MAX_BYTES = 100 * 1024;
29
+ /** Paths per upload or move batch, like the code-mode input paths and the 64-item request limits. */
30
+ export const AI_LOCAL_BATCH_MAX_ITEMS = 64;
31
+ /** AGENTS.md text one turn carries, the Codex default. */
32
+ export const AI_LOCAL_INSTRUCTIONS_MAX_BYTES = 32 * 1024;
33
+ /** Local Skills one turn lists, like the 64-item request limits. */
34
+ export const AI_LOCAL_SKILLS_MAX_ITEMS = 64;
35
+ /** Local Skills one message selects explicitly. */
36
+ export const AI_LOCAL_SELECTED_SKILLS_MAX_ITEMS = 4;
37
+ /** Files a loaded local Skill lists, like Cloud Skill references. */
38
+ export const AI_LOCAL_SKILL_FILES_MAX_ITEMS = 50;
39
+
40
+ const LocalPath = z.string().min(1).max(AI_LOCAL_PATH_MAX_CHARS);
41
+ const ChatPath = z.string().trim().min(1).max(500);
42
+ const SkillName = z.string().regex(AI_SKILL_NAME_PATTERN).max(64);
43
+ /** One line of client-reported text: control characters and line breaks never reach the prompt. */
44
+ const FactLine = (max: number) =>
45
+ z
46
+ .string()
47
+ .max(max)
48
+ .transform((value) => value.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu, " ").trim())
49
+ .pipe(z.string().min(1));
50
+
51
+ /** A request the client did not run: the person said no, a rule refused it, or it failed. */
52
+ const LocalToolProblem = z.object({
53
+ status: z.enum(["denied", "failed"]),
54
+ error: z.string().max(2000),
55
+ });
56
+
57
+ export const CloudAiLocalListInputSchema = z.object({
58
+ path: LocalPath.default(".").describe("Folder to list, relative to the workspace."),
59
+ pattern: z
60
+ .string()
61
+ .trim()
62
+ .min(1)
63
+ .max(500)
64
+ .optional()
65
+ .describe("Glob such as **/*.pdf or 2026-09/*.{pdf,jpg}; matching ignores case. ** lists subfolders."),
66
+ recursive: z.boolean().default(false),
67
+ limit: z.number().int().min(1).max(AI_LOCAL_LIST_MAX_LIMIT).default(AI_LOCAL_LIST_DEFAULT_LIMIT),
68
+ after: LocalPath.optional().describe("nextAfter of the previous page."),
69
+ });
70
+ export const CloudAiLocalListOutputSchema = z.discriminatedUnion("status", [
71
+ z.object({
72
+ status: z.literal("completed"),
73
+ path: LocalPath,
74
+ entries: z
75
+ .array(
76
+ z.object({
77
+ path: LocalPath,
78
+ type: z.enum(["file", "directory", "other"]),
79
+ size: z.number().int().min(0),
80
+ modifiedAt: z.string().max(40),
81
+ }),
82
+ )
83
+ .max(AI_LOCAL_LIST_MAX_LIMIT),
84
+ nextAfter: LocalPath.nullable(),
85
+ unreadable: z.array(LocalPath).max(AI_LOCAL_LIST_MAX_LIMIT).optional(),
86
+ }),
87
+ LocalToolProblem,
88
+ ]);
89
+
90
+ export const CloudAiLocalReadInputSchema = z.object({
91
+ path: LocalPath,
92
+ offset: z.number().int().min(1).default(1).describe("First line to read."),
93
+ maxBytes: z.number().int().min(1).max(AI_LOCAL_READ_MAX_BYTES).default(AI_LOCAL_READ_DEFAULT_BYTES),
94
+ });
95
+ // Two completed shapes share their status, so this is a plain union rather than one keyed by status.
96
+ export const CloudAiLocalReadOutputSchema = z.union([
97
+ z.object({
98
+ status: z.literal("completed"),
99
+ kind: z.literal("text"),
100
+ path: LocalPath,
101
+ /** Lines prefixed with their number and a tab; short lines can make the numbers outweigh the text. */
102
+ content: z.string().max(AI_LOCAL_READ_MAX_BYTES * 10),
103
+ firstLine: z.number().int().min(1),
104
+ lastLine: z.number().int().min(0),
105
+ totalBytes: z.number().int().min(0),
106
+ nextOffset: z.number().int().min(1).nullable(),
107
+ }),
108
+ z.object({
109
+ status: z.literal("completed"),
110
+ kind: z.literal("uploaded"),
111
+ path: LocalPath,
112
+ chatPath: ChatPath,
113
+ mediaType: z.string().max(120),
114
+ size: z.number().int().min(0),
115
+ }),
116
+ LocalToolProblem,
117
+ ]);
118
+
119
+ export const CloudAiLocalUploadInputSchema = z.object({
120
+ paths: z.array(LocalPath).min(1).max(AI_LOCAL_BATCH_MAX_ITEMS),
121
+ directory: ChatPath.optional().describe("Chat folder for all files; by default each file keeps its folder from the workspace."),
122
+ });
123
+ export const CloudAiLocalUploadOutputSchema = z.discriminatedUnion("status", [
124
+ z.object({
125
+ status: z.literal("completed"),
126
+ files: z
127
+ .array(z.object({ path: LocalPath, chatPath: ChatPath, mediaType: z.string().max(120), size: z.number().int().min(0) }))
128
+ .max(AI_LOCAL_BATCH_MAX_ITEMS),
129
+ failed: z.array(z.object({ path: LocalPath, error: z.string().max(2000) })).max(AI_LOCAL_BATCH_MAX_ITEMS),
130
+ }),
131
+ LocalToolProblem,
132
+ ]);
133
+
134
+ export const CloudAiLocalDownloadInputSchema = z.object({
135
+ path: ChatPath.describe("Chat file to save."),
136
+ to: LocalPath.describe("Target file, relative to the workspace."),
137
+ replace: z.boolean().default(false),
138
+ });
139
+ export const CloudAiLocalDownloadOutputSchema = z.discriminatedUnion("status", [
140
+ z.object({ status: z.literal("completed"), path: LocalPath, size: z.number().int().min(0), replaced: z.boolean() }),
141
+ LocalToolProblem,
142
+ ]);
143
+
144
+ export const CloudAiLocalWriteInputSchema = z.object({
145
+ path: LocalPath,
146
+ content: z.string().refine((value) => new TextEncoder().encode(value).byteLength <= AI_LOCAL_WRITE_MAX_BYTES, "Content exceeds 100 KiB."),
147
+ mode: z.enum(["create", "append", "replace"]).default("create"),
148
+ });
149
+ export const CloudAiLocalWriteOutputSchema = z.discriminatedUnion("status", [
150
+ z.object({
151
+ status: z.literal("completed"),
152
+ path: LocalPath,
153
+ mode: z.enum(["create", "append", "replace"]),
154
+ size: z.number().int().min(0),
155
+ }),
156
+ LocalToolProblem,
157
+ ]);
158
+
159
+ const LocalMove = z.object({ from: LocalPath, to: LocalPath });
160
+ export const CloudAiLocalMoveInputSchema = z.object({
161
+ moves: z.array(LocalMove).min(1).max(AI_LOCAL_BATCH_MAX_ITEMS),
162
+ });
163
+ /** A move batch reports what already moved even when it stops, so the model can name or undo it. */
164
+ export const CloudAiLocalMoveOutputSchema = z.object({
165
+ status: z.enum(["completed", "denied", "failed"]),
166
+ moved: z.array(LocalMove).max(AI_LOCAL_BATCH_MAX_ITEMS),
167
+ conflicts: z.array(z.object({ to: LocalPath, reason: z.string().max(500) })).max(AI_LOCAL_BATCH_MAX_ITEMS),
168
+ error: z.string().max(2000).optional(),
169
+ });
170
+
171
+ export const CloudAiLoadLocalSkillInputSchema = z.object({ name: SkillName });
172
+ export const CloudAiLoadLocalSkillOutputSchema = z.discriminatedUnion("status", [
173
+ z.object({
174
+ status: z.literal("completed"),
175
+ name: SkillName,
176
+ directory: LocalPath,
177
+ instructions: z.string().max(AI_SKILL_INSTRUCTIONS_MAX_CHARS),
178
+ files: z.array(LocalPath).max(AI_LOCAL_SKILL_FILES_MAX_ITEMS),
179
+ }),
180
+ LocalToolProblem,
181
+ ]);
182
+
183
+ /** Facts about the local client a turn comes from; Cloud renders them into the "# Local client" prompt section. */
184
+ export const AiLocalClientSchema = z.object({
185
+ kind: z.enum(["cli", "acp"]),
186
+ /** The editor's ACP clientInfo.name. */
187
+ name: FactLine(120).optional(),
188
+ cwd: FactLine(AI_LOCAL_PATH_MAX_CHARS),
189
+ os: FactLine(120),
190
+ shell: FactLine(120),
191
+ localTools: z.enum(["ask", "off"]),
192
+ });
193
+
194
+ /** AGENTS.md files and local Skills the client found in its workspace for one turn. */
195
+ export const AiLocalContextSchema = z.object({
196
+ instructions: z
197
+ .array(z.object({ path: LocalPath, content: z.string() }))
198
+ .max(64)
199
+ .refine(
200
+ (files) =>
201
+ files.reduce((bytes, file) => bytes + new TextEncoder().encode(file.content).byteLength, 0) <= AI_LOCAL_INSTRUCTIONS_MAX_BYTES,
202
+ "AGENTS.md content exceeds 32 KiB.",
203
+ )
204
+ .default([]),
205
+ skills: z
206
+ .array(
207
+ z.object({
208
+ name: SkillName,
209
+ description: z.string().trim().min(1).max(AI_SKILL_DESCRIPTION_MAX_CHARS),
210
+ scope: z.enum(["project", "global"]),
211
+ manualOnly: z.boolean().optional(),
212
+ }),
213
+ )
214
+ .max(AI_LOCAL_SKILLS_MAX_ITEMS)
215
+ .refine((skills) => new Set(skills.map((skill) => skill.name)).size === skills.length, "Local Skill names must be unique.")
216
+ .default([]),
217
+ selectedSkills: z
218
+ .array(
219
+ z.object({
220
+ name: SkillName,
221
+ directory: LocalPath,
222
+ instructions: z.string().trim().min(1).max(AI_SKILL_INSTRUCTIONS_MAX_CHARS),
223
+ }),
224
+ )
225
+ .max(AI_LOCAL_SELECTED_SKILLS_MAX_ITEMS)
226
+ .default([]),
227
+ });
228
+
229
+ export type CloudAiLocalListInput = z.infer<typeof CloudAiLocalListInputSchema>;
230
+ export type CloudAiLocalListOutput = z.infer<typeof CloudAiLocalListOutputSchema>;
231
+ export type CloudAiLocalReadInput = z.infer<typeof CloudAiLocalReadInputSchema>;
232
+ export type CloudAiLocalReadOutput = z.infer<typeof CloudAiLocalReadOutputSchema>;
233
+ export type CloudAiLocalUploadInput = z.infer<typeof CloudAiLocalUploadInputSchema>;
234
+ export type CloudAiLocalUploadOutput = z.infer<typeof CloudAiLocalUploadOutputSchema>;
235
+ export type CloudAiLocalDownloadInput = z.infer<typeof CloudAiLocalDownloadInputSchema>;
236
+ export type CloudAiLocalDownloadOutput = z.infer<typeof CloudAiLocalDownloadOutputSchema>;
237
+ export type CloudAiLocalWriteInput = z.infer<typeof CloudAiLocalWriteInputSchema>;
238
+ export type CloudAiLocalWriteOutput = z.infer<typeof CloudAiLocalWriteOutputSchema>;
239
+ export type CloudAiLocalMoveInput = z.infer<typeof CloudAiLocalMoveInputSchema>;
240
+ export type CloudAiLocalMoveOutput = z.infer<typeof CloudAiLocalMoveOutputSchema>;
241
+ export type CloudAiLoadLocalSkillInput = z.infer<typeof CloudAiLoadLocalSkillInputSchema>;
242
+ export type CloudAiLoadLocalSkillOutput = z.infer<typeof CloudAiLoadLocalSkillOutputSchema>;
243
+ export type AiLocalClient = z.infer<typeof AiLocalClientSchema>;
244
+ export type AiLocalContext = z.infer<typeof AiLocalContextSchema>;