@sentry/junior 0.46.0 → 0.47.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 (2) hide show
  1. package/dist/app.js +486 -226
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -4466,6 +4466,29 @@ function truncateText(value, maxChars = MAX_TEXT_CHARS) {
4466
4466
  truncated: true
4467
4467
  };
4468
4468
  }
4469
+ function isMissingPathError(error) {
4470
+ if (!error || typeof error !== "object") {
4471
+ return false;
4472
+ }
4473
+ const candidate = error;
4474
+ if (candidate.code === "ENOENT") {
4475
+ return true;
4476
+ }
4477
+ return typeof candidate.message === "string" && candidate.message.startsWith("File not found:");
4478
+ }
4479
+ function missingPathSearchResult(params) {
4480
+ const textPath = params.displayPath ?? params.missingPath ?? params.path;
4481
+ return {
4482
+ content: [{ type: "text", text: `Path not found: ${textPath}` }],
4483
+ details: {
4484
+ ok: false,
4485
+ error: "not_found",
4486
+ path: params.path,
4487
+ ...params.missingPath ? { missing_path: params.missingPath } : {},
4488
+ truncated: false
4489
+ }
4490
+ };
4491
+ }
4469
4492
  function escapeRegExp(value) {
4470
4493
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4471
4494
  }
@@ -4520,13 +4543,35 @@ function resolveWorkspacePath(input, fallback = ".") {
4520
4543
  async function collectFiles(params) {
4521
4544
  const files = [];
4522
4545
  let limitReached = false;
4546
+ let missingPath;
4523
4547
  const visit = async (dirPath) => {
4524
- const entries = (await params.fs.readdir(dirPath)).sort(
4525
- (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
4526
- );
4548
+ if (missingPath) {
4549
+ return;
4550
+ }
4551
+ let entries;
4552
+ try {
4553
+ entries = (await params.fs.readdir(dirPath)).sort(
4554
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
4555
+ );
4556
+ } catch (error) {
4557
+ if (isMissingPathError(error)) {
4558
+ missingPath = dirPath;
4559
+ return;
4560
+ }
4561
+ throw error;
4562
+ }
4527
4563
  for (const entry of entries) {
4528
4564
  const fullPath = path4.posix.join(dirPath, entry);
4529
- const stat2 = await params.fs.stat(fullPath);
4565
+ let stat2;
4566
+ try {
4567
+ stat2 = await params.fs.stat(fullPath);
4568
+ } catch (error) {
4569
+ if (isMissingPathError(error)) {
4570
+ missingPath = fullPath;
4571
+ return;
4572
+ }
4573
+ throw error;
4574
+ }
4530
4575
  if (stat2.isDirectory()) {
4531
4576
  if (!SKIPPED_DIRECTORIES.has(entry)) {
4532
4577
  await visit(fullPath);
@@ -4544,20 +4589,38 @@ async function collectFiles(params) {
4544
4589
  }
4545
4590
  }
4546
4591
  };
4547
- const stat = await params.fs.stat(params.root);
4592
+ let stat;
4593
+ try {
4594
+ stat = await params.fs.stat(params.root);
4595
+ } catch (error) {
4596
+ if (isMissingPathError(error)) {
4597
+ return {
4598
+ files,
4599
+ limitReached: false,
4600
+ missingPath: params.root,
4601
+ missingRoot: true
4602
+ };
4603
+ }
4604
+ throw error;
4605
+ }
4548
4606
  if (!stat.isDirectory()) {
4549
4607
  const relativePath = path4.posix.basename(params.root);
4550
4608
  return {
4551
4609
  files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
4552
- limitReached: false
4610
+ limitReached: false,
4611
+ missingRoot: false
4553
4612
  };
4554
4613
  }
4555
4614
  await visit(params.root);
4556
- return { files, limitReached };
4615
+ return {
4616
+ files,
4617
+ limitReached,
4618
+ missingPath,
4619
+ missingRoot: missingPath === params.root
4620
+ };
4557
4621
  }
4558
4622
 
4559
- // src/chat/tools/sandbox/edit-file.ts
4560
- import { Type as Type2 } from "@sinclair/typebox";
4623
+ // src/chat/tools/sandbox/text-edits.ts
4561
4624
  function detectLineEnding(value) {
4562
4625
  return value.includes("\r\n") ? "\r\n" : "\n";
4563
4626
  }
@@ -4589,6 +4652,44 @@ function firstChangedLine(oldContent, newContent) {
4589
4652
  }
4590
4653
  return void 0;
4591
4654
  }
4655
+ function prepareTextReplacementArguments(input) {
4656
+ if (!input || typeof input !== "object") {
4657
+ return input;
4658
+ }
4659
+ const raw = { ...input };
4660
+ if (typeof raw.edits === "string") {
4661
+ try {
4662
+ raw.edits = JSON.parse(raw.edits);
4663
+ } catch {
4664
+ return raw;
4665
+ }
4666
+ }
4667
+ const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
4668
+ const oldText = raw.oldText ?? raw.old_text;
4669
+ const newText = raw.newText ?? raw.new_text;
4670
+ if (typeof oldText === "string" && typeof newText === "string") {
4671
+ edits.push({ oldText, newText });
4672
+ }
4673
+ if (edits.length > 0) {
4674
+ raw.edits = edits.map((edit) => {
4675
+ if (!edit || typeof edit !== "object") {
4676
+ return edit;
4677
+ }
4678
+ const record = edit;
4679
+ const { old_text, new_text, ...rest } = record;
4680
+ return {
4681
+ ...rest,
4682
+ oldText: record.oldText ?? old_text,
4683
+ newText: record.newText ?? new_text
4684
+ };
4685
+ });
4686
+ }
4687
+ delete raw.oldText;
4688
+ delete raw.old_text;
4689
+ delete raw.newText;
4690
+ delete raw.new_text;
4691
+ return raw;
4692
+ }
4592
4693
  function buildCompactDiff(oldContent, newContent) {
4593
4694
  const oldLines = oldContent.split("\n");
4594
4695
  const newLines = newContent.split("\n");
@@ -4630,19 +4731,19 @@ function buildCompactDiff(oldContent, newContent) {
4630
4731
  firstChangedLine: firstChangedLine(oldContent, newContent)
4631
4732
  };
4632
4733
  }
4633
- function validateAndApplyEdits(content, edits, filePath) {
4734
+ function validateAndApplyTextEdits(content, edits, targetName) {
4634
4735
  if (!Array.isArray(edits) || edits.length === 0) {
4635
- throw new Error("editFile requires at least one edit.");
4736
+ throw new Error(`${targetName} requires at least one edit.`);
4636
4737
  }
4637
4738
  const normalizedEdits = edits.map((edit, index) => {
4638
4739
  if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
4639
4740
  throw new Error(
4640
- `edits[${index}].oldText must not be empty in ${filePath}.`
4741
+ `edits[${index}].oldText must not be empty in ${targetName}.`
4641
4742
  );
4642
4743
  }
4643
4744
  if (typeof edit.newText !== "string") {
4644
4745
  throw new Error(
4645
- `edits[${index}].newText must be a string in ${filePath}.`
4746
+ `edits[${index}].newText must be a string in ${targetName}.`
4646
4747
  );
4647
4748
  }
4648
4749
  return {
@@ -4656,13 +4757,13 @@ function validateAndApplyEdits(content, edits, filePath) {
4656
4757
  const matchIndex = content.indexOf(edit.oldText);
4657
4758
  if (matchIndex === -1) {
4658
4759
  throw new Error(
4659
- `Could not find edits[${index}] in ${filePath}. oldText must match exactly including whitespace and newlines.`
4760
+ `Could not find edits[${index}] in ${targetName}. oldText must match exactly including whitespace and newlines.`
4660
4761
  );
4661
4762
  }
4662
4763
  const occurrences = countOccurrences(content, edit.oldText);
4663
4764
  if (occurrences > 1) {
4664
4765
  throw new Error(
4665
- `Found ${occurrences} occurrences of edits[${index}] in ${filePath}. Each oldText must be unique.`
4766
+ `Found ${occurrences} occurrences of edits[${index}] in ${targetName}. Each oldText must be unique.`
4666
4767
  );
4667
4768
  }
4668
4769
  matchedEdits.push({
@@ -4678,7 +4779,7 @@ function validateAndApplyEdits(content, edits, filePath) {
4678
4779
  const current = matchedEdits[index];
4679
4780
  if (previous.matchIndex + previous.matchLength > current.matchIndex) {
4680
4781
  throw new Error(
4681
- `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${filePath}. Merge overlapping replacements into one edit.`
4782
+ `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${targetName}. Merge overlapping replacements into one edit.`
4682
4783
  );
4683
4784
  }
4684
4785
  }
@@ -4688,47 +4789,15 @@ function validateAndApplyEdits(content, edits, filePath) {
4688
4789
  newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
4689
4790
  }
4690
4791
  if (newContent === content) {
4691
- throw new Error(`No changes made to ${filePath}.`);
4792
+ throw new Error(`No changes made to ${targetName}.`);
4692
4793
  }
4693
4794
  return { baseContent: content, newContent };
4694
4795
  }
4796
+
4797
+ // src/chat/tools/sandbox/edit-file.ts
4798
+ import { Type as Type2 } from "@sinclair/typebox";
4695
4799
  function prepareEditFileArguments(input) {
4696
- if (!input || typeof input !== "object") {
4697
- return input;
4698
- }
4699
- const raw = { ...input };
4700
- if (typeof raw.edits === "string") {
4701
- try {
4702
- raw.edits = JSON.parse(raw.edits);
4703
- } catch {
4704
- return raw;
4705
- }
4706
- }
4707
- const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
4708
- const oldText = raw.oldText ?? raw.old_text;
4709
- const newText = raw.newText ?? raw.new_text;
4710
- if (typeof oldText === "string" && typeof newText === "string") {
4711
- edits.push({ oldText, newText });
4712
- }
4713
- if (edits.length > 0) {
4714
- raw.edits = edits.map((edit) => {
4715
- if (!edit || typeof edit !== "object") {
4716
- return edit;
4717
- }
4718
- const record = edit;
4719
- const { old_text, new_text, ...rest } = record;
4720
- return {
4721
- ...rest,
4722
- oldText: record.oldText ?? old_text,
4723
- newText: record.newText ?? new_text
4724
- };
4725
- });
4726
- }
4727
- delete raw.oldText;
4728
- delete raw.old_text;
4729
- delete raw.newText;
4730
- delete raw.new_text;
4731
- return raw;
4800
+ return prepareTextReplacementArguments(input);
4732
4801
  }
4733
4802
  async function editFile(params) {
4734
4803
  const filePath = resolveWorkspacePath(params.path);
@@ -4736,7 +4805,7 @@ async function editFile(params) {
4736
4805
  const { bom, text } = stripBom(rawContent);
4737
4806
  const lineEnding = detectLineEnding(text);
4738
4807
  const normalizedContent = normalizeToLf(text);
4739
- const { baseContent, newContent } = validateAndApplyEdits(
4808
+ const { baseContent, newContent } = validateAndApplyTextEdits(
4740
4809
  normalizedContent,
4741
4810
  params.edits,
4742
4811
  params.path
@@ -4817,12 +4886,18 @@ async function findFiles(params) {
4817
4886
  }
4818
4887
  const root = resolveWorkspacePath(params.path);
4819
4888
  const limit = positiveInteger(params.limit) ?? DEFAULT_FIND_LIMIT;
4820
- const { files, limitReached } = await collectFiles({
4889
+ const { files, limitReached, missingPath, missingRoot } = await collectFiles({
4821
4890
  fs: params.fs,
4822
4891
  root,
4823
4892
  pattern: params.pattern,
4824
4893
  limit
4825
4894
  });
4895
+ if (missingPath) {
4896
+ return missingPathSearchResult({
4897
+ path: params.path ?? ".",
4898
+ ...missingRoot ? { displayPath: params.path ?? "." } : { missingPath }
4899
+ });
4900
+ }
4826
4901
  const relativePaths = files.map(
4827
4902
  (filePath) => path5.posix.relative(root, filePath)
4828
4903
  );
@@ -4919,11 +4994,17 @@ async function grepFiles(params) {
4919
4994
  const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
4920
4995
  const context = positiveInteger(params.context) ?? 0;
4921
4996
  const regex = params.literal ? void 0 : new RegExp(params.pattern, params.ignoreCase ? "i" : "");
4922
- const { files } = await collectFiles({
4997
+ const { files, missingPath, missingRoot } = await collectFiles({
4923
4998
  fs: params.fs,
4924
4999
  root,
4925
5000
  pattern: params.glob
4926
5001
  });
5002
+ if (missingPath) {
5003
+ return missingPathSearchResult({
5004
+ path: params.path ?? ".",
5005
+ ...missingRoot ? { displayPath: params.path ?? "." } : { missingPath }
5006
+ });
5007
+ }
4927
5008
  const output = [];
4928
5009
  let matchCount = 0;
4929
5010
  let matchLimitReached = false;
@@ -4933,8 +5014,14 @@ async function grepFiles(params) {
4933
5014
  let content;
4934
5015
  try {
4935
5016
  content = await params.fs.readFile(filePath, { encoding: "utf8" });
4936
- } catch {
4937
- continue;
5017
+ } catch (error) {
5018
+ if (isMissingPathError(error)) {
5019
+ return missingPathSearchResult({
5020
+ path: params.path ?? ".",
5021
+ missingPath: filePath
5022
+ });
5023
+ }
5024
+ throw error;
4938
5025
  }
4939
5026
  if (content.includes("\0")) {
4940
5027
  continue;
@@ -5214,13 +5301,29 @@ var DEFAULT_LIST_LIMIT = 500;
5214
5301
  async function listDir(params) {
5215
5302
  const dirPath = resolveWorkspacePath(params.path);
5216
5303
  const limit = positiveInteger(params.limit) ?? DEFAULT_LIST_LIMIT;
5217
- const stat = await params.fs.stat(dirPath);
5304
+ let stat;
5305
+ try {
5306
+ stat = await params.fs.stat(dirPath);
5307
+ } catch (error) {
5308
+ if (isMissingPathError(error)) {
5309
+ return missingPathSearchResult({ path: params.path ?? "." });
5310
+ }
5311
+ throw error;
5312
+ }
5218
5313
  if (!stat.isDirectory()) {
5219
5314
  throw new Error(`Not a directory: ${params.path ?? "."}`);
5220
5315
  }
5221
- const entries = (await params.fs.readdir(dirPath)).sort(
5222
- (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
5223
- );
5316
+ let entries;
5317
+ try {
5318
+ entries = (await params.fs.readdir(dirPath)).sort(
5319
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
5320
+ );
5321
+ } catch (error) {
5322
+ if (isMissingPathError(error)) {
5323
+ return missingPathSearchResult({ path: params.path ?? "." });
5324
+ }
5325
+ throw error;
5326
+ }
5224
5327
  const output = [];
5225
5328
  let entryLimitReached = false;
5226
5329
  for (const entry of entries) {
@@ -5232,8 +5335,14 @@ async function listDir(params) {
5232
5335
  try {
5233
5336
  const entryStat = await params.fs.stat(entryPath);
5234
5337
  output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
5235
- } catch {
5236
- continue;
5338
+ } catch (error) {
5339
+ if (isMissingPathError(error)) {
5340
+ return missingPathSearchResult({
5341
+ path: params.path ?? ".",
5342
+ missingPath: entryPath
5343
+ });
5344
+ }
5345
+ throw error;
5237
5346
  }
5238
5347
  }
5239
5348
  const bounded = truncateText(
@@ -5824,6 +5933,14 @@ function sliceFileContent(params) {
5824
5933
  } : {}
5825
5934
  };
5826
5935
  }
5936
+ function missingFileResult(path11) {
5937
+ return {
5938
+ content: "",
5939
+ error: "not_found",
5940
+ path: path11,
5941
+ success: false
5942
+ };
5943
+ }
5827
5944
  function createReadFileTool() {
5828
5945
  return tool({
5829
5946
  description: "Read a bounded line range from a file in the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Prefer grep/findFiles/listDir for broad discovery.",
@@ -6180,6 +6297,37 @@ function createSlackMessageAddReactionTool(context, state) {
6180
6297
  // src/chat/tools/slack/canvas-tools.ts
6181
6298
  import { Type as Type16 } from "@sinclair/typebox";
6182
6299
 
6300
+ // src/chat/slack/canvas-references.ts
6301
+ var SLACK_FILE_ID_PATTERN = /^F[A-Z0-9]{4,}$/i;
6302
+ var SLACK_CANVAS_PATH_PATTERN = /^\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]{4,})(?:\/|$)/i;
6303
+ function isSlackHost(hostname) {
6304
+ return hostname === "slack.com" || hostname.endsWith(".slack.com");
6305
+ }
6306
+ function normalizeReferenceInput(input) {
6307
+ let value = input.trim();
6308
+ if (value.startsWith("<") && value.endsWith(">")) {
6309
+ value = value.slice(1, -1);
6310
+ }
6311
+ return value.split("|", 1)[0]?.trim() ?? "";
6312
+ }
6313
+ function extractCanvasId(input) {
6314
+ const trimmed = normalizeReferenceInput(input);
6315
+ if (!trimmed) return void 0;
6316
+ if (SLACK_FILE_ID_PATTERN.test(trimmed)) {
6317
+ return trimmed.toUpperCase();
6318
+ }
6319
+ try {
6320
+ const url = new URL(trimmed);
6321
+ if (!isSlackHost(url.hostname)) {
6322
+ return void 0;
6323
+ }
6324
+ const urlMatch = url.pathname.match(SLACK_CANVAS_PATH_PATTERN);
6325
+ return urlMatch?.[1]?.toUpperCase();
6326
+ } catch {
6327
+ return void 0;
6328
+ }
6329
+ }
6330
+
6183
6331
  // src/chat/tools/slack/canvases.ts
6184
6332
  function normalizeCanvasMarkdown(markdown) {
6185
6333
  let normalizedHeadingCount = 0;
@@ -6271,27 +6419,7 @@ async function grantChannelCanvasAccess(canvasId, channelId) {
6271
6419
  );
6272
6420
  }
6273
6421
  }
6274
- async function lookupCanvasSection(canvasId, containsText) {
6275
- const client2 = getSlackClient();
6276
- const response = await withSlackRetries(
6277
- () => client2.canvases.sections.lookup({
6278
- canvas_id: canvasId,
6279
- criteria: {
6280
- contains_text: containsText
6281
- }
6282
- }),
6283
- 3,
6284
- {
6285
- action: "canvases.sections.lookup",
6286
- attributes: {
6287
- "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
6288
- "app.slack.canvas.contains_text_length": containsText.length
6289
- }
6290
- }
6291
- );
6292
- return response.sections?.[0]?.id;
6293
- }
6294
- async function updateCanvas(input) {
6422
+ async function writeCanvasMarkdown(input) {
6295
6423
  const client2 = getSlackClient();
6296
6424
  const normalizedContent = normalizeCanvasMarkdown(input.markdown);
6297
6425
  await withSlackRetries(
@@ -6299,8 +6427,7 @@ async function updateCanvas(input) {
6299
6427
  canvas_id: input.canvasId,
6300
6428
  changes: [
6301
6429
  {
6302
- operation: input.operation,
6303
- section_id: input.sectionId,
6430
+ operation: "replace",
6304
6431
  document_content: {
6305
6432
  type: "markdown",
6306
6433
  markdown: normalizedContent.markdown
@@ -6313,27 +6440,19 @@ async function updateCanvas(input) {
6313
6440
  action: "canvases.edit",
6314
6441
  attributes: {
6315
6442
  "app.slack.canvas.canvas_id_prefix": input.canvasId.slice(0, 1),
6316
- "app.slack.canvas.operation": input.operation,
6443
+ "app.slack.canvas.operation": "replace",
6317
6444
  "app.slack.canvas.markdown_length": normalizedContent.markdown.length,
6318
6445
  "app.slack.canvas.markdown_normalized": normalizedContent.normalizedHeadingCount > 0,
6319
6446
  "app.slack.canvas.normalized_heading_count": normalizedContent.normalizedHeadingCount
6320
6447
  }
6321
6448
  }
6322
6449
  );
6450
+ return normalizedContent;
6323
6451
  }
6324
- var CANVAS_ID_PATTERN = /^F[A-Z0-9]+$/i;
6325
- var CANVAS_URL_FILE_ID_PATTERN = /\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]+)/i;
6326
- function extractCanvasId(input) {
6327
- const trimmed = input.trim();
6328
- if (!trimmed) return void 0;
6329
- if (CANVAS_ID_PATTERN.test(trimmed)) {
6330
- return trimmed.toUpperCase();
6331
- }
6332
- const urlMatch = trimmed.match(CANVAS_URL_FILE_ID_PATTERN);
6333
- if (urlMatch?.[1]) {
6334
- return urlMatch[1].toUpperCase();
6335
- }
6336
- return void 0;
6452
+ function isCanvasFile(file) {
6453
+ const filetype = file.filetype?.toLowerCase() ?? "";
6454
+ const mimetype = file.mimetype?.toLowerCase() ?? "";
6455
+ return filetype === "quip" || filetype === "canvas" || mimetype.includes("quip") || mimetype.includes("canvas");
6337
6456
  }
6338
6457
  async function readCanvas(canvasIdOrUrl) {
6339
6458
  const canvasId = extractCanvasId(canvasIdOrUrl);
@@ -6359,6 +6478,9 @@ async function readCanvas(canvasIdOrUrl) {
6359
6478
  if (!file) {
6360
6479
  throw new Error("Slack returned no file metadata for canvas.");
6361
6480
  }
6481
+ if (!isCanvasFile(file)) {
6482
+ throw new Error("Slack file metadata did not describe a Canvas document.");
6483
+ }
6362
6484
  const downloadUrl = file.url_private_download ?? file.url_private;
6363
6485
  if (!downloadUrl) {
6364
6486
  throw new Error(
@@ -6378,7 +6500,6 @@ async function readCanvas(canvasIdOrUrl) {
6378
6500
  }
6379
6501
 
6380
6502
  // src/chat/tools/slack/canvas-tools.ts
6381
- var MAX_CANVAS_READ_CHARS = 4e4;
6382
6503
  var MAX_RECENT_CANVASES = 5;
6383
6504
  function mergeRecentCanvases(existing, created) {
6384
6505
  const nextEntry = {
@@ -6391,6 +6512,46 @@ function mergeRecentCanvases(existing, created) {
6391
6512
  const deduped = prior.filter((entry) => entry.id !== created.id);
6392
6513
  return [nextEntry, ...deduped].slice(0, MAX_RECENT_CANVASES);
6393
6514
  }
6515
+ function prepareCanvasEditArguments(input) {
6516
+ return prepareTextReplacementArguments(input);
6517
+ }
6518
+ function storedCanvasUrl(state, canvasId) {
6519
+ const lastCanvasUrl = state.artifactState.lastCanvasUrl;
6520
+ if (lastCanvasUrl && extractCanvasId(lastCanvasUrl) === canvasId) {
6521
+ return lastCanvasUrl;
6522
+ }
6523
+ for (const canvas of state.artifactState.recentCanvases ?? []) {
6524
+ if (extractCanvasId(canvas.id) === canvasId) {
6525
+ return canvas.url;
6526
+ }
6527
+ if (canvas.url && extractCanvasId(canvas.url) === canvasId) {
6528
+ return canvas.url;
6529
+ }
6530
+ }
6531
+ return void 0;
6532
+ }
6533
+ function resolveCanvasTarget(canvas) {
6534
+ const canvasId = extractCanvasId(canvas);
6535
+ if (!canvasId) {
6536
+ return {
6537
+ ok: false,
6538
+ error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
6539
+ };
6540
+ }
6541
+ return { ok: true, canvasId };
6542
+ }
6543
+ var editReplacementSchema2 = Type16.Object(
6544
+ {
6545
+ oldText: Type16.String({
6546
+ minLength: 1,
6547
+ description: "Exact Canvas markdown to replace. It must be unique in the current Canvas body and must not overlap another edit."
6548
+ }),
6549
+ newText: Type16.String({
6550
+ description: "Replacement Canvas markdown for this edit."
6551
+ })
6552
+ },
6553
+ { additionalProperties: false }
6554
+ );
6394
6555
  function createSlackCanvasCreateTool(context, state) {
6395
6556
  return tool({
6396
6557
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when the answer is better as a reusable document than a thread reply: long-form research, timelines, bios/profiles, structured notes, plans, comparisons, or anything likely to exceed one compact Slack reply. After creating it, reply with one or two short sentences plus the canvas link; do not recap the canvas contents. Do not use for short answers that fit cleanly in one normal thread reply.",
@@ -6439,7 +6600,6 @@ function createSlackCanvasCreateTool(context, state) {
6439
6600
  markdown,
6440
6601
  channelId: targetChannelId
6441
6602
  });
6442
- state.setTurnCreatedCanvasId(created.canvasId);
6443
6603
  await state.patchArtifactState({
6444
6604
  lastCanvasId: created.canvasId,
6445
6605
  lastCanvasUrl: created.permalink,
@@ -6463,67 +6623,111 @@ function createSlackCanvasCreateTool(context, state) {
6463
6623
  }
6464
6624
  });
6465
6625
  }
6466
- function createSlackCanvasUpdateTool(state, _context) {
6626
+ function createSlackCanvasReadTool() {
6467
6627
  return tool({
6468
- description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
6469
- inputSchema: Type16.Object({
6470
- markdown: Type16.String({
6471
- minLength: 1,
6472
- description: "Markdown content to insert or use as replacement text."
6473
- }),
6474
- operation: Type16.Optional(
6475
- Type16.Union(
6476
- [
6477
- Type16.Literal("insert_at_end"),
6478
- Type16.Literal("insert_at_start"),
6479
- Type16.Literal("replace")
6480
- ],
6481
- { description: "Canvas update mode." }
6482
- )
6483
- ),
6484
- section_id: Type16.Optional(
6485
- Type16.String({
6486
- minLength: 1,
6487
- description: "Optional section ID required for targeted replace operations."
6488
- })
6489
- ),
6490
- section_contains_text: Type16.Optional(
6491
- Type16.String({
6628
+ description: "Read a bounded line range from a Slack canvas as markdown. Use when you need exact Canvas contents to verify facts or make edits safely. Do not use for generic web pages \u2014 use webFetch for those.",
6629
+ annotations: { readOnlyHint: true, destructiveHint: false },
6630
+ inputSchema: Type16.Object(
6631
+ {
6632
+ canvas: Type16.String({
6492
6633
  minLength: 1,
6493
- description: "Optional helper text used to find the target section when section_id is not provided."
6494
- })
6495
- )
6496
- }),
6497
- execute: async ({
6498
- markdown,
6499
- operation,
6500
- section_id,
6501
- section_contains_text
6502
- }) => {
6503
- const targetCanvasId = state.getTurnCreatedCanvasId() ?? state.getCurrentCanvasId();
6504
- const resolvedOperation = operation ?? "insert_at_end";
6505
- if (!targetCanvasId) {
6634
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
6635
+ }),
6636
+ offset: Type16.Optional(
6637
+ Type16.Integer({
6638
+ minimum: 1,
6639
+ description: "1-indexed line number to start reading from."
6640
+ })
6641
+ ),
6642
+ limit: Type16.Optional(
6643
+ Type16.Integer({
6644
+ minimum: 1,
6645
+ description: "Maximum number of lines to read. Defaults to 1000."
6646
+ })
6647
+ )
6648
+ },
6649
+ { additionalProperties: false }
6650
+ ),
6651
+ execute: async ({ canvas, offset, limit }) => {
6652
+ const target = resolveCanvasTarget(canvas);
6653
+ if (!target.ok) {
6654
+ return target;
6655
+ }
6656
+ try {
6657
+ const result = await readCanvas(target.canvasId);
6658
+ const range = sliceFileContent({
6659
+ content: normalizeToLf(result.content),
6660
+ limit,
6661
+ offset,
6662
+ path: result.canvasId
6663
+ });
6664
+ return {
6665
+ ok: true,
6666
+ canvas_id: result.canvasId,
6667
+ title: result.title,
6668
+ permalink: result.permalink,
6669
+ mimetype: result.mimetype,
6670
+ filetype: result.filetype,
6671
+ original_byte_length: result.byteLength,
6672
+ content: range.content,
6673
+ start_line: range.start_line,
6674
+ end_line: range.end_line,
6675
+ total_lines: range.total_lines,
6676
+ truncated: range.truncated,
6677
+ continuation: range.continuation
6678
+ };
6679
+ } catch (error) {
6680
+ const message = error instanceof Error ? error.message : "canvas read failed";
6506
6681
  logWarn(
6507
- "slack_canvas_update_missing_target",
6682
+ "slack_canvas_read_failed",
6508
6683
  {},
6509
6684
  {
6510
- "gen_ai.tool.name": "slackCanvasUpdate",
6511
- "app.artifacts.last_canvas_id": state.artifactState.lastCanvasId ?? "none",
6512
- "app.artifacts.turn_created_canvas_id": state.getTurnCreatedCanvasId() ?? "none"
6685
+ "gen_ai.tool.name": "slackCanvasRead",
6686
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6513
6687
  },
6514
- "Canvas update rejected because no explicit target canvas was provided"
6688
+ message
6515
6689
  );
6516
6690
  return {
6517
6691
  ok: false,
6518
- error: "No active canvas found in artifact context"
6692
+ canvas_id: target.canvasId,
6693
+ error: message
6519
6694
  };
6520
6695
  }
6521
- const operationKey = createOperationKey("slackCanvasUpdate", {
6522
- canvas_id: targetCanvasId,
6523
- markdown,
6524
- operation: resolvedOperation,
6525
- section_id: section_id ?? null,
6526
- section_contains_text: section_contains_text ?? null
6696
+ }
6697
+ });
6698
+ }
6699
+ function createSlackCanvasEditTool(state) {
6700
+ return tool({
6701
+ description: "Edit one Slack canvas with exact markdown replacements. Use for precise changes to existing Canvas content. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff.",
6702
+ promptSnippet: "existing-canvas exact edits; returns diff",
6703
+ promptGuidelines: [
6704
+ "prefer over slackCanvasWrite for targeted changes",
6705
+ "oldText exact, unique, non-overlapping",
6706
+ "multiple same-canvas changes: one edits[] call"
6707
+ ],
6708
+ prepareArguments: prepareCanvasEditArguments,
6709
+ executionMode: "sequential",
6710
+ inputSchema: Type16.Object(
6711
+ {
6712
+ canvas: Type16.String({
6713
+ minLength: 1,
6714
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
6715
+ }),
6716
+ edits: Type16.Array(editReplacementSchema2, {
6717
+ minItems: 1,
6718
+ description: "Exact replacements matched against the current Canvas body, not incrementally."
6719
+ })
6720
+ },
6721
+ { additionalProperties: false }
6722
+ ),
6723
+ execute: async ({ canvas, edits }) => {
6724
+ const target = resolveCanvasTarget(canvas);
6725
+ if (!target.ok) {
6726
+ return target;
6727
+ }
6728
+ const operationKey = createOperationKey("slackCanvasEdit", {
6729
+ canvas_id: target.canvasId,
6730
+ edits
6527
6731
  });
6528
6732
  const cached = state.getOperationResult(operationKey);
6529
6733
  if (cached) {
@@ -6532,72 +6736,124 @@ function createSlackCanvasUpdateTool(state, _context) {
6532
6736
  deduplicated: true
6533
6737
  };
6534
6738
  }
6535
- const sectionId = section_id ?? (section_contains_text ? await lookupCanvasSection(targetCanvasId, section_contains_text) : void 0);
6536
- await updateCanvas({
6537
- canvasId: targetCanvasId,
6538
- markdown,
6539
- operation: resolvedOperation,
6540
- sectionId
6541
- });
6542
- await state.patchArtifactState({ lastCanvasId: targetCanvasId });
6543
- const response = {
6544
- ok: true,
6545
- canvas_id: targetCanvasId,
6546
- operation: resolvedOperation,
6547
- section_id: sectionId
6548
- };
6549
- state.setOperationResult(operationKey, response);
6550
- return response;
6739
+ try {
6740
+ const current = await readCanvas(target.canvasId);
6741
+ const normalizedContent = normalizeToLf(current.content);
6742
+ const { baseContent, newContent } = validateAndApplyTextEdits(
6743
+ normalizedContent,
6744
+ edits,
6745
+ target.canvasId
6746
+ );
6747
+ const written = await writeCanvasMarkdown({
6748
+ canvasId: target.canvasId,
6749
+ markdown: newContent
6750
+ });
6751
+ await state.patchArtifactState({
6752
+ lastCanvasId: target.canvasId,
6753
+ lastCanvasUrl: current.permalink ?? state.artifactState.lastCanvasUrl
6754
+ });
6755
+ const diff = buildCompactDiff(
6756
+ normalizeCanvasMarkdown(baseContent).markdown,
6757
+ written.markdown
6758
+ );
6759
+ const response = {
6760
+ ok: true,
6761
+ canvas_id: target.canvasId,
6762
+ title: current.title,
6763
+ permalink: current.permalink,
6764
+ diff: diff.diff,
6765
+ first_changed_line: diff.firstChangedLine,
6766
+ replacements: edits.length,
6767
+ normalized_heading_count: written.normalizedHeadingCount,
6768
+ summary: `Edited canvas ${target.canvasId}`
6769
+ };
6770
+ state.setOperationResult(operationKey, response);
6771
+ return response;
6772
+ } catch (error) {
6773
+ const message = error instanceof Error ? error.message : "canvas edit failed";
6774
+ logWarn(
6775
+ "slack_canvas_edit_failed",
6776
+ {},
6777
+ {
6778
+ "gen_ai.tool.name": "slackCanvasEdit",
6779
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6780
+ },
6781
+ message
6782
+ );
6783
+ return {
6784
+ ok: false,
6785
+ canvas_id: target.canvasId,
6786
+ error: message
6787
+ };
6788
+ }
6551
6789
  }
6552
6790
  });
6553
6791
  }
6554
- function createSlackCanvasReadTool() {
6792
+ function createSlackCanvasWriteTool(state) {
6555
6793
  return tool({
6556
- description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
6557
- annotations: { readOnlyHint: true, destructiveHint: false },
6558
- inputSchema: Type16.Object({
6559
- canvas: Type16.String({
6560
- minLength: 1,
6561
- description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
6562
- })
6563
- }),
6564
- execute: async ({ canvas }) => {
6565
- const canvasId = extractCanvasId(canvas);
6566
- if (!canvasId) {
6794
+ description: "Write UTF-8 markdown content to a Slack canvas. Use for deliberate full-Canvas replacement after validation. Do not use for targeted edits.",
6795
+ promptSnippet: "deliberate full-canvas replacement",
6796
+ promptGuidelines: ["targeted existing-canvas changes: slackCanvasEdit"],
6797
+ executionMode: "sequential",
6798
+ inputSchema: Type16.Object(
6799
+ {
6800
+ canvas: Type16.String({
6801
+ minLength: 1,
6802
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
6803
+ }),
6804
+ content: Type16.String({
6805
+ description: "UTF-8 markdown content to write."
6806
+ })
6807
+ },
6808
+ { additionalProperties: false }
6809
+ ),
6810
+ execute: async ({ canvas, content }) => {
6811
+ const target = resolveCanvasTarget(canvas);
6812
+ if (!target.ok) {
6813
+ return target;
6814
+ }
6815
+ const operationKey = createOperationKey("slackCanvasWrite", {
6816
+ canvas_id: target.canvasId,
6817
+ content
6818
+ });
6819
+ const cached = state.getOperationResult(operationKey);
6820
+ if (cached) {
6567
6821
  return {
6568
- ok: false,
6569
- error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
6822
+ ...cached,
6823
+ deduplicated: true
6570
6824
  };
6571
6825
  }
6572
6826
  try {
6573
- const result = await readCanvas(canvas);
6574
- const truncated = result.content.length > MAX_CANVAS_READ_CHARS;
6575
- const content = truncated ? result.content.slice(0, MAX_CANVAS_READ_CHARS) : result.content;
6576
- return {
6827
+ const written = await writeCanvasMarkdown({
6828
+ canvasId: target.canvasId,
6829
+ markdown: content
6830
+ });
6831
+ await state.patchArtifactState({
6832
+ lastCanvasId: target.canvasId,
6833
+ lastCanvasUrl: storedCanvasUrl(state, target.canvasId)
6834
+ });
6835
+ const response = {
6577
6836
  ok: true,
6578
- canvas_id: result.canvasId,
6579
- title: result.title,
6580
- permalink: result.permalink,
6581
- mimetype: result.mimetype,
6582
- filetype: result.filetype,
6583
- original_byte_length: result.byteLength,
6584
- truncated,
6585
- content
6837
+ canvas_id: target.canvasId,
6838
+ normalized_heading_count: written.normalizedHeadingCount,
6839
+ summary: `Wrote canvas ${target.canvasId}`
6586
6840
  };
6841
+ state.setOperationResult(operationKey, response);
6842
+ return response;
6587
6843
  } catch (error) {
6588
- const message = error instanceof Error ? error.message : "canvas read failed";
6844
+ const message = error instanceof Error ? error.message : "canvas write failed";
6589
6845
  logWarn(
6590
- "slack_canvas_read_failed",
6846
+ "slack_canvas_write_failed",
6591
6847
  {},
6592
6848
  {
6593
- "gen_ai.tool.name": "slackCanvasRead",
6594
- "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
6849
+ "gen_ai.tool.name": "slackCanvasWrite",
6850
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6595
6851
  },
6596
6852
  message
6597
6853
  );
6598
6854
  return {
6599
6855
  ok: false,
6600
- canvas_id: canvasId,
6856
+ canvas_id: target.canvasId,
6601
6857
  error: message
6602
6858
  };
6603
6859
  }
@@ -8529,7 +8785,6 @@ function createWriteFileTool() {
8529
8785
  // src/chat/tools/index.ts
8530
8786
  function createToolState(hooks, context) {
8531
8787
  const operationResultCache = /* @__PURE__ */ new Map();
8532
- let turnCreatedCanvasId;
8533
8788
  const artifactState = {
8534
8789
  ...context.artifactState ?? {},
8535
8790
  listColumnMap: {
@@ -8549,11 +8804,6 @@ function createToolState(hooks, context) {
8549
8804
  return {
8550
8805
  artifactState,
8551
8806
  patchArtifactState,
8552
- getCurrentCanvasId: () => artifactState.lastCanvasId,
8553
- getTurnCreatedCanvasId: () => turnCreatedCanvasId,
8554
- setTurnCreatedCanvasId: (canvasId) => {
8555
- turnCreatedCanvasId = canvasId;
8556
- },
8557
8807
  getCurrentListId: () => artifactState.lastListId,
8558
8808
  getOperationResult: (operationKey) => operationResultCache.get(operationKey),
8559
8809
  setOperationResult: (operationKey, result) => {
@@ -8584,7 +8834,8 @@ function createTools(availableSkills, hooks = {}, context) {
8584
8834
  hooks.toolOverrides?.imageGenerate
8585
8835
  ),
8586
8836
  slackCanvasRead: createSlackCanvasReadTool(),
8587
- slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
8837
+ slackCanvasEdit: createSlackCanvasEditTool(state),
8838
+ slackCanvasWrite: createSlackCanvasWriteTool(state),
8588
8839
  slackThreadRead: createSlackThreadReadTool(context),
8589
8840
  slackUserLookup: createSlackUserLookupTool(),
8590
8841
  slackListCreate: createSlackListCreateTool(state),
@@ -10304,7 +10555,16 @@ function createSandboxExecutor(options) {
10304
10555
  "app.sandbox.path.length": filePath.length
10305
10556
  },
10306
10557
  async () => {
10307
- const response = await executeReadFile({ path: filePath });
10558
+ let response;
10559
+ try {
10560
+ response = await executeReadFile({ path: filePath });
10561
+ } catch (error) {
10562
+ if (isMissingPathError(error)) {
10563
+ setSpanStatus("ok");
10564
+ return missingFileResult(filePath);
10565
+ }
10566
+ throw error;
10567
+ }
10308
10568
  const content = String(response.content ?? "");
10309
10569
  setSpanAttributes({
10310
10570
  "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
@@ -16363,24 +16623,14 @@ function createSlackTurnRuntime(deps) {
16363
16623
  const threadId = deps.getThreadId(thread, message);
16364
16624
  const channelId = deps.getChannelId(thread, message);
16365
16625
  const runId = deps.getRunId(thread, message);
16366
- const context = logContext({
16626
+ const turnContext = logContext({
16367
16627
  threadId,
16368
16628
  requesterId: message.author.userId,
16369
16629
  requesterUserName: message.author.userName,
16370
16630
  channelId,
16371
16631
  runId
16372
16632
  });
16373
- processingReaction = await startSlackProcessingReaction({
16374
- thread,
16375
- message,
16376
- logException: deps.logException,
16377
- logContext: context
16378
- });
16379
- const toolInvocationHook = createToolInvocationHook(
16380
- processingReaction,
16381
- hooks
16382
- );
16383
- await deps.withSpan("chat.turn", "chat.turn", context, async () => {
16633
+ await deps.withSpan("chat.turn", "chat.turn", turnContext, async () => {
16384
16634
  const legacyAttachmentText = renderSlackLegacyAttachmentText(
16385
16635
  message.raw
16386
16636
  );
@@ -16395,7 +16645,7 @@ function createSlackTurnRuntime(deps) {
16395
16645
  strippedUserText,
16396
16646
  message.raw
16397
16647
  );
16398
- const context2 = {
16648
+ const threadContext = {
16399
16649
  threadId,
16400
16650
  requesterId: message.author.userId,
16401
16651
  channelId,
@@ -16413,7 +16663,7 @@ function createSlackTurnRuntime(deps) {
16413
16663
  thread,
16414
16664
  message,
16415
16665
  decision: { shouldReply: false, reason },
16416
- context: context2,
16666
+ context: threadContext,
16417
16667
  userText
16418
16668
  });
16419
16669
  return;
@@ -16423,7 +16673,7 @@ function createSlackTurnRuntime(deps) {
16423
16673
  message,
16424
16674
  userText,
16425
16675
  explicitMention: Boolean(message.isMention),
16426
- context: context2
16676
+ context: threadContext
16427
16677
  });
16428
16678
  await deps.persistPreparedState({
16429
16679
  thread,
@@ -16435,7 +16685,7 @@ function createSlackTurnRuntime(deps) {
16435
16685
  conversationContext: deps.getPreparedConversationContext(preparedState),
16436
16686
  hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
16437
16687
  isExplicitMention: Boolean(message.isMention),
16438
- context: context2
16688
+ context: threadContext
16439
16689
  });
16440
16690
  if (await maybeHandleThreadOptOutDecision({
16441
16691
  thread,
@@ -16446,7 +16696,7 @@ function createSlackTurnRuntime(deps) {
16446
16696
  thread,
16447
16697
  message,
16448
16698
  decision,
16449
- context: context2,
16699
+ context: threadContext,
16450
16700
  preparedState,
16451
16701
  userText
16452
16702
  });
@@ -16457,12 +16707,22 @@ function createSlackTurnRuntime(deps) {
16457
16707
  thread,
16458
16708
  message,
16459
16709
  decision,
16460
- context: context2,
16710
+ context: threadContext,
16461
16711
  preparedState,
16462
16712
  userText
16463
16713
  });
16464
16714
  return;
16465
16715
  }
16716
+ processingReaction = await startSlackProcessingReaction({
16717
+ thread,
16718
+ message,
16719
+ logException: deps.logException,
16720
+ logContext: turnContext
16721
+ });
16722
+ const toolInvocationHook = createToolInvocationHook(
16723
+ processingReaction,
16724
+ hooks
16725
+ );
16466
16726
  await deps.replyToThread(thread, message, {
16467
16727
  explicitMention: Boolean(message.isMention),
16468
16728
  preparedState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"