@hasna/assistants 1.1.78 → 1.1.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -293,6 +293,18 @@ function deepMerge(base, override) {
293
293
  }
294
294
 
295
295
  // packages/core/src/config.ts
296
+ var exports_config = {};
297
+ __export(exports_config, {
298
+ loadSystemPrompt: () => loadSystemPrompt,
299
+ loadHooksConfig: () => loadHooksConfig,
300
+ loadConfig: () => loadConfig,
301
+ getTempFolder: () => getTempFolder,
302
+ getProjectDataDir: () => getProjectDataDir,
303
+ getProjectConfigDir: () => getProjectConfigDir,
304
+ getConfigPath: () => getConfigPath,
305
+ getConfigDir: () => getConfigDir,
306
+ ensureConfigDir: () => ensureConfigDir
307
+ });
296
308
  import { join } from "path";
297
309
  import { homedir } from "os";
298
310
  function mergeConnectorsConfig(base, override) {
@@ -380,6 +392,28 @@ function migrateConfigKeys(config) {
380
392
  }
381
393
  return config;
382
394
  }
395
+ async function loadHooksConfig(cwd = process.cwd(), baseDir) {
396
+ const hooks = {};
397
+ const userHooksPath = getConfigPath("hooks.json", baseDir);
398
+ const userHooks = await loadJsonFile(userHooksPath);
399
+ if (userHooks?.hooks) {
400
+ mergeHooks(hooks, userHooks.hooks);
401
+ }
402
+ const projectHooksPath = join(getProjectConfigDir(cwd), "hooks.json");
403
+ const projectHooks = await loadJsonFile(projectHooksPath);
404
+ if (projectHooks?.hooks) {
405
+ mergeHooks(hooks, projectHooks.hooks);
406
+ }
407
+ return hooks;
408
+ }
409
+ function mergeHooks(target, source) {
410
+ for (const [event, matchers] of Object.entries(source)) {
411
+ if (!target[event]) {
412
+ target[event] = [];
413
+ }
414
+ target[event].push(...matchers);
415
+ }
416
+ }
383
417
  async function loadJsonFile(path) {
384
418
  try {
385
419
  if (!hasRuntime()) {
@@ -417,6 +451,9 @@ async function ensureConfigDir(sessionId, baseDir) {
417
451
  }
418
452
  await Promise.all(dirs);
419
453
  }
454
+ function getTempFolder(sessionId, baseDir) {
455
+ return join(baseDir || getConfigDir(), "temp", sessionId);
456
+ }
420
457
  async function loadSystemPrompt(cwd = process.cwd(), baseDir) {
421
458
  const prompts = [];
422
459
  const globalPromptPath = getConfigPath("ASSISTANTS.md", baseDir);
@@ -712,7 +749,15 @@ var init_config = __esm(async () => {
712
749
  },
713
750
  mode: "placeholder"
714
751
  }
715
- }
752
+ },
753
+ workspace: {
754
+ mode: "sandbox",
755
+ customPath: null
756
+ },
757
+ permissions: {
758
+ bash: "readonly"
759
+ },
760
+ backgroundModel: "claude-haiku-4-5-20251001"
716
761
  };
717
762
  });
718
763
 
@@ -24692,7 +24737,7 @@ var init_bash = __esm(async () => {
24692
24737
  BashTool = class BashTool {
24693
24738
  static tool = {
24694
24739
  name: "bash",
24695
- description: "Execute a shell command. RESTRICTED to read-only operations by default (ls, cat, grep, find, git status/log/diff, pwd, which, echo). Set validation.perTool.bash.allowAll=true to allow broader commands.",
24740
+ description: 'Execute a shell command. Permission level is controlled by permissions.bash in config: "none" (disabled), "readonly" (default \u2014 ls, cat, grep, find, git status/log/diff, pwd, which, echo), or "readwrite" (broader commands, destructive ops still blocked).',
24696
24741
  parameters: {
24697
24742
  type: "object",
24698
24743
  properties: {
@@ -24810,6 +24855,86 @@ var init_bash = __esm(async () => {
24810
24855
  /^connect-[a-z0-9._-]+$/i,
24811
24856
  /^@hasna\/[a-z0-9._-]+$/i
24812
24857
  ];
24858
+ static READWRITE_ALLOWED_COMMANDS = [
24859
+ "mkdir",
24860
+ "touch",
24861
+ "cp",
24862
+ "mv",
24863
+ "ln",
24864
+ "tee",
24865
+ "sed",
24866
+ "awk",
24867
+ "tar",
24868
+ "zip",
24869
+ "unzip",
24870
+ "gzip",
24871
+ "gunzip",
24872
+ "bzip2",
24873
+ "bunzip2",
24874
+ "git add",
24875
+ "git commit",
24876
+ "git push",
24877
+ "git pull",
24878
+ "git checkout",
24879
+ "git switch",
24880
+ "git merge",
24881
+ "git rebase",
24882
+ "git stash",
24883
+ "git cherry-pick",
24884
+ "git revert",
24885
+ "git reset",
24886
+ "git restore",
24887
+ "git init",
24888
+ "git clone",
24889
+ "git fetch",
24890
+ "git branch",
24891
+ "npm",
24892
+ "pnpm",
24893
+ "yarn",
24894
+ "bun",
24895
+ "pip",
24896
+ "pip3",
24897
+ "make",
24898
+ "cmake",
24899
+ "kill",
24900
+ "pkill",
24901
+ "chmod",
24902
+ "chown",
24903
+ "docker",
24904
+ "node",
24905
+ "bun",
24906
+ "npx",
24907
+ "bunx",
24908
+ "tsx",
24909
+ "ts-node",
24910
+ "python",
24911
+ "python3",
24912
+ "rm",
24913
+ "rmdir"
24914
+ ];
24915
+ static READWRITE_BLOCKED_PATTERNS = [
24916
+ /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~\/?\s|\.\.\/)/,
24917
+ /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~\/?\s|\.\.\/)/,
24918
+ /\brm\s+-rf\s+\/(?!\S)/,
24919
+ /\brm\s+-rf\s+~\/?(?:\s|$)/,
24920
+ /\bmkfs\b/,
24921
+ /\bdd\b/,
24922
+ /\bfdisk\b/,
24923
+ /\bparted\b/,
24924
+ /\bsudo\b/,
24925
+ /\bsu\b/,
24926
+ /\bdoas\b/,
24927
+ /\bchmod\s+777\b/,
24928
+ /curl.*\|\s*(bash|sh)/,
24929
+ /wget.*\|\s*(bash|sh)/,
24930
+ /\bnc\s+-l/,
24931
+ /\bnetcat\s+-l/,
24932
+ /\bvim?\b/,
24933
+ /\bnano\b/,
24934
+ /\bemacs\b/,
24935
+ /:\(\)\s*\{/,
24936
+ /\bfork\s*bomb/i
24937
+ ];
24813
24938
  static getBunGlobalInstallInfo(command) {
24814
24939
  const parts = this.splitCommandByOperators(command);
24815
24940
  if (parts.length !== 1) {
@@ -25045,16 +25170,22 @@ var init_bash = __esm(async () => {
25045
25170
  let allowEnv = true;
25046
25171
  let allowAll = false;
25047
25172
  let allowPackageInstall = false;
25173
+ let bashPermission = "readonly";
25048
25174
  try {
25049
25175
  const config = await loadConfig(cwd);
25050
25176
  const bashConfig = config.validation?.perTool?.bash;
25051
25177
  allowEnv = bashConfig?.allowEnv ?? true;
25052
25178
  allowAll = bashConfig?.allowAll ?? false;
25053
25179
  allowPackageInstall = bashConfig?.allowPackageInstall ?? false;
25180
+ bashPermission = config.permissions?.bash ?? "readonly";
25054
25181
  } catch {
25055
25182
  allowEnv = true;
25056
25183
  allowAll = false;
25057
25184
  allowPackageInstall = false;
25185
+ bashPermission = "readonly";
25186
+ }
25187
+ if (bashPermission === "none") {
25188
+ throw toolPermissionDenied("bash", 'Bash is disabled. Change permissions.bash in config to "readonly" or "readwrite" to enable.', input);
25058
25189
  }
25059
25190
  const baseCommand = command.replace(/\s*2>&1\s*/g, " ").trim();
25060
25191
  const baseTrimmed = baseCommand.toLowerCase();
@@ -25079,7 +25210,8 @@ var init_bash = __esm(async () => {
25079
25210
  throw toolPermissionDenied("bash", securityCheck.reason || "Blocked command", input);
25080
25211
  }
25081
25212
  if (!allowAll && !allowGlobalInstall) {
25082
- for (const pattern of this.BLOCKED_PATTERNS) {
25213
+ const blockedPatterns = bashPermission === "readwrite" ? this.READWRITE_BLOCKED_PATTERNS : this.BLOCKED_PATTERNS;
25214
+ for (const pattern of blockedPatterns) {
25083
25215
  if (pattern.test(commandSansQuotes)) {
25084
25216
  getSecurityLogger().log({
25085
25217
  eventType: "blocked_command",
@@ -25091,7 +25223,8 @@ var init_bash = __esm(async () => {
25091
25223
  },
25092
25224
  sessionId: input.sessionId || "unknown"
25093
25225
  });
25094
- throw toolPermissionDenied("bash", "This command is not allowed. Only read-only commands are permitted (ls, cat, grep, find, git status/log/diff, etc.)", input);
25226
+ const modeLabel = bashPermission === "readwrite" ? "readwrite" : "readonly";
25227
+ throw toolPermissionDenied("bash", `Blocked: command is not allowed in ${modeLabel} mode. ${bashPermission === "readonly" ? "Only read-only commands are permitted (ls, cat, grep, find, git status/log/diff, etc.)." : "Destructive operations (rm -rf /, mkfs, dd, sudo, etc.) are blocked even in readwrite mode."}`, input);
25095
25228
  }
25096
25229
  }
25097
25230
  }
@@ -25111,7 +25244,10 @@ var init_bash = __esm(async () => {
25111
25244
  throw toolPermissionDenied("bash", "Command not allowed: env/printenv disabled by config.", input);
25112
25245
  }
25113
25246
  if (!allowAll && !allowGlobalInstall) {
25114
- const allowlist = allowEnv ? this.ALLOWED_COMMANDS : this.ALLOWED_COMMANDS.filter((allowed) => allowed !== "env" && allowed !== "printenv");
25247
+ let allowlist = allowEnv ? this.ALLOWED_COMMANDS : this.ALLOWED_COMMANDS.filter((allowed) => allowed !== "env" && allowed !== "printenv");
25248
+ if (bashPermission === "readwrite") {
25249
+ allowlist = [...allowlist, ...this.READWRITE_ALLOWED_COMMANDS];
25250
+ }
25115
25251
  const isAllowed = this.areAllCommandPartsAllowed(commandForChecks, allowlist);
25116
25252
  if (!isAllowed) {
25117
25253
  getSecurityLogger().log({
@@ -25124,7 +25260,8 @@ var init_bash = __esm(async () => {
25124
25260
  },
25125
25261
  sessionId: input.sessionId || "unknown"
25126
25262
  });
25127
- throw toolPermissionDenied("bash", "Command not in allowed list. Permitted commands: cat, head, tail, ls, find, grep, wc, file, stat, pwd, which, echo, curl, git status/log/diff/branch/show, connect-*", input);
25263
+ const modeLabel = bashPermission === "readwrite" ? "readwrite" : "readonly";
25264
+ throw toolPermissionDenied("bash", `Blocked: command is not allowed in ${modeLabel} mode. ${bashPermission === "readonly" ? "Permitted commands: cat, head, tail, ls, find, grep, wc, file, stat, pwd, which, echo, curl, git status/log/diff/branch/show, connect-*" : "The command is not in the readwrite allowlist. Destructive system operations remain blocked."}`, input);
25128
25265
  }
25129
25266
  const ssrfCheck = await this.validateCurlSsrf(commandForChecks);
25130
25267
  if (!ssrfCheck.valid) {
@@ -25400,7 +25537,23 @@ function isInScriptsFolder(path2, cwd, sessionId) {
25400
25537
  return true;
25401
25538
  return resolved.startsWith(`${scriptsFolder}${sep}`);
25402
25539
  }
25403
- var currentSessionId = "default", FilesystemTools;
25540
+ function isDangerousPath(resolvedPath) {
25541
+ const parts = resolvedPath.split(sep);
25542
+ for (const dir of DANGEROUS_DIRS) {
25543
+ if (parts.includes(dir)) {
25544
+ return `Writing to '${dir}' is not allowed for safety reasons`;
25545
+ }
25546
+ }
25547
+ return null;
25548
+ }
25549
+ function isWithinDir(filePath, rootDir) {
25550
+ const resolvedFile = resolve4(filePath);
25551
+ const resolvedRoot = resolve4(rootDir);
25552
+ if (resolvedFile === resolvedRoot)
25553
+ return true;
25554
+ return resolvedFile.startsWith(`${resolvedRoot}${sep}`);
25555
+ }
25556
+ var currentSessionId = "default", currentWorkspaceConfig, DANGEROUS_DIRS, FilesystemTools;
25404
25557
  var init_filesystem = __esm(async () => {
25405
25558
  init_errors();
25406
25559
  init_paths();
@@ -25411,6 +25564,17 @@ var init_filesystem = __esm(async () => {
25411
25564
  init_runtime(),
25412
25565
  init_logger()
25413
25566
  ]);
25567
+ currentWorkspaceConfig = { mode: "sandbox", customPath: null };
25568
+ DANGEROUS_DIRS = [
25569
+ "node_modules",
25570
+ ".git",
25571
+ ".svn",
25572
+ ".hg",
25573
+ ".DS_Store",
25574
+ "__pycache__",
25575
+ ".env",
25576
+ ".venv"
25577
+ ];
25414
25578
  FilesystemTools = class FilesystemTools {
25415
25579
  static resolveInputPath(baseCwd, inputPath) {
25416
25580
  const envHome = process.env.HOME || process.env.USERPROFILE;
@@ -25423,10 +25587,13 @@ var init_filesystem = __esm(async () => {
25423
25587
  }
25424
25588
  return resolve4(baseCwd, inputPath);
25425
25589
  }
25426
- static registerAll(registry, sessionId) {
25590
+ static registerAll(registry, sessionId, workspaceConfig) {
25427
25591
  if (sessionId) {
25428
25592
  currentSessionId = sessionId;
25429
25593
  }
25594
+ if (workspaceConfig) {
25595
+ currentWorkspaceConfig = workspaceConfig;
25596
+ }
25430
25597
  registry.register(this.readTool, this.readExecutor);
25431
25598
  registry.register(this.writeTool, this.writeExecutor);
25432
25599
  registry.register(this.globTool, this.globExecutor);
@@ -25608,17 +25775,17 @@ var init_filesystem = __esm(async () => {
25608
25775
  };
25609
25776
  static writeTool = {
25610
25777
  name: "write",
25611
- description: "Write content to a file. RESTRICTED: Can only write to the project scripts folder (.assistants-data/scripts/{session}). Provide a filename (or path) and it will be saved under the scripts folder.",
25778
+ description: 'Write content to a file. Behavior depends on workspace.mode in config: "sandbox" (default) restricts writes to .assistants-data/scripts/{session}/, "project" allows writing anywhere in the project directory, "custom" allows writing to a configured custom path. Dangerous directories (node_modules, .git, etc.) are always blocked.',
25612
25779
  parameters: {
25613
25780
  type: "object",
25614
25781
  properties: {
25615
25782
  filename: {
25616
25783
  type: "string",
25617
- description: "The filename to write to (saved in the project scripts folder)"
25784
+ description: "The filename to write to. In sandbox mode, saved in the project scripts folder. In project/custom mode, can be a relative or absolute path within the workspace."
25618
25785
  },
25619
25786
  path: {
25620
25787
  type: "string",
25621
- description: "Alias for filename (saved in the project scripts folder)"
25788
+ description: "Alias for filename"
25622
25789
  },
25623
25790
  content: {
25624
25791
  type: "string",
@@ -25650,7 +25817,6 @@ var init_filesystem = __esm(async () => {
25650
25817
  suggestion: "Provide string content to write."
25651
25818
  });
25652
25819
  }
25653
- const scriptsFolder = getScriptsFolder(baseCwd, input.sessionId);
25654
25820
  if (!filename || !filename.trim()) {
25655
25821
  throw new ToolExecutionError("Filename or path is required", {
25656
25822
  toolName: "write",
@@ -25661,20 +25827,83 @@ var init_filesystem = __esm(async () => {
25661
25827
  suggestion: "Provide a filename and try again."
25662
25828
  });
25663
25829
  }
25664
- const sanitizedFilename = filename.replace(/\.\.[/\\]/g, "").replace(/\.\./g, "").replace(/^[/\\]+/, "");
25665
- const path2 = join14(scriptsFolder, sanitizedFilename);
25666
- if (!isInScriptsFolder(path2, baseCwd, input.sessionId)) {
25667
- throw new ToolExecutionError(`Cannot write outside scripts folder. Files are saved to ${scriptsFolder}`, {
25830
+ const mode = currentWorkspaceConfig.mode || "sandbox";
25831
+ let resolvedPath;
25832
+ let allowedRoot;
25833
+ if (mode === "sandbox") {
25834
+ const scriptsFolder = getScriptsFolder(baseCwd, input.sessionId);
25835
+ const sanitizedFilename = filename.replace(/\.\.[/\\]/g, "").replace(/\.\./g, "").replace(/^[/\\]+/, "");
25836
+ resolvedPath = join14(scriptsFolder, sanitizedFilename);
25837
+ allowedRoot = scriptsFolder;
25838
+ if (!isInScriptsFolder(resolvedPath, baseCwd, input.sessionId)) {
25839
+ throw new ToolExecutionError(`Cannot write outside scripts folder. Files are saved to ${scriptsFolder}`, {
25840
+ toolName: "write",
25841
+ toolInput: input,
25842
+ code: ErrorCodes.TOOL_PERMISSION_DENIED,
25843
+ recoverable: false,
25844
+ retryable: false,
25845
+ suggestion: "Write only within the project scripts folder."
25846
+ });
25847
+ }
25848
+ } else if (mode === "project") {
25849
+ resolvedPath = FilesystemTools.resolveInputPath(baseCwd, filename);
25850
+ allowedRoot = resolve4(baseCwd);
25851
+ if (!isWithinDir(resolvedPath, allowedRoot)) {
25852
+ throw new ToolExecutionError(`Cannot write outside the project directory (${allowedRoot}). Use workspace.mode "custom" with a customPath to write elsewhere.`, {
25853
+ toolName: "write",
25854
+ toolInput: input,
25855
+ code: ErrorCodes.TOOL_PERMISSION_DENIED,
25856
+ recoverable: false,
25857
+ retryable: false,
25858
+ suggestion: "Provide a path within the project directory."
25859
+ });
25860
+ }
25861
+ } else if (mode === "custom") {
25862
+ const customPath = currentWorkspaceConfig.customPath;
25863
+ if (!customPath) {
25864
+ throw new ToolExecutionError('Workspace mode is "custom" but no customPath is configured. Set workspace.customPath in your config.', {
25865
+ toolName: "write",
25866
+ toolInput: input,
25867
+ code: ErrorCodes.TOOL_EXECUTION_FAILED,
25868
+ recoverable: false,
25869
+ retryable: false,
25870
+ suggestion: "Set workspace.customPath to an absolute path in config."
25871
+ });
25872
+ }
25873
+ allowedRoot = resolve4(customPath);
25874
+ resolvedPath = FilesystemTools.resolveInputPath(allowedRoot, filename);
25875
+ if (!isWithinDir(resolvedPath, allowedRoot)) {
25876
+ throw new ToolExecutionError(`Cannot write outside the custom workspace (${allowedRoot}).`, {
25877
+ toolName: "write",
25878
+ toolInput: input,
25879
+ code: ErrorCodes.TOOL_PERMISSION_DENIED,
25880
+ recoverable: false,
25881
+ retryable: false,
25882
+ suggestion: `Provide a path within ${allowedRoot}.`
25883
+ });
25884
+ }
25885
+ } else {
25886
+ throw new ToolExecutionError(`Unknown workspace mode: "${mode}". Valid modes: sandbox, project, custom.`, {
25887
+ toolName: "write",
25888
+ toolInput: input,
25889
+ code: ErrorCodes.VALIDATION_OUT_OF_RANGE,
25890
+ recoverable: false,
25891
+ retryable: false
25892
+ });
25893
+ }
25894
+ const dangerousReason = isDangerousPath(resolvedPath);
25895
+ if (dangerousReason) {
25896
+ throw new ToolExecutionError(dangerousReason, {
25668
25897
  toolName: "write",
25669
25898
  toolInput: input,
25670
25899
  code: ErrorCodes.TOOL_PERMISSION_DENIED,
25671
25900
  recoverable: false,
25672
25901
  retryable: false,
25673
- suggestion: "Write only within the project scripts folder."
25902
+ suggestion: "Choose a different target path that avoids system directories."
25674
25903
  });
25675
25904
  }
25676
25905
  try {
25677
- const validated = await validatePath(path2, { allowSymlinks: false, allowedPaths: [scriptsFolder] });
25906
+ const validated = await validatePath(resolvedPath, { allowSymlinks: false, allowedPaths: [allowedRoot] });
25678
25907
  if (!validated.valid) {
25679
25908
  throw new ToolExecutionError(validated.error || "Invalid path", {
25680
25909
  toolName: "write",
@@ -25682,7 +25911,7 @@ var init_filesystem = __esm(async () => {
25682
25911
  code: ErrorCodes.VALIDATION_OUT_OF_RANGE,
25683
25912
  recoverable: false,
25684
25913
  retryable: false,
25685
- suggestion: "Write only within the allowed scripts folder."
25914
+ suggestion: `Write only within the allowed workspace (${allowedRoot}).`
25686
25915
  });
25687
25916
  }
25688
25917
  const safety = await isPathSafe(validated.resolved, "write", { cwd: baseCwd });
@@ -100744,6 +100973,7 @@ class BuiltinCommands {
100744
100973
  loader.register(this.ordersCommand());
100745
100974
  loader.register(this.tasksCommand());
100746
100975
  loader.register(this.setupCommand());
100976
+ loader.register(this.scriptsCommand());
100747
100977
  loader.register(this.exitCommand());
100748
100978
  loader.register(this.diffCommand());
100749
100979
  loader.register(this.undoCommand());
@@ -109573,6 +109803,73 @@ Not a git repository or git not available.
109573
109803
  }
109574
109804
  };
109575
109805
  }
109806
+ scriptsCommand() {
109807
+ return {
109808
+ name: "scripts",
109809
+ description: "List generated files in the sandbox folder",
109810
+ builtin: true,
109811
+ selfHandled: true,
109812
+ content: "",
109813
+ handler: async (_args, context) => {
109814
+ const { getProjectDataDir: getDataDir } = await init_config().then(() => exports_config);
109815
+ const { readdirSync: readdirSync9, statSync: statSync6 } = await import("fs");
109816
+ const scriptsRoot = join26(getDataDir(context.cwd), "scripts", context.sessionId);
109817
+ let entries = [];
109818
+ const walk = (dir, prefix) => {
109819
+ let items;
109820
+ try {
109821
+ items = readdirSync9(dir);
109822
+ } catch {
109823
+ return;
109824
+ }
109825
+ for (const item of items) {
109826
+ const fullPath = join26(dir, item);
109827
+ try {
109828
+ const stat5 = statSync6(fullPath);
109829
+ if (stat5.isDirectory()) {
109830
+ walk(fullPath, prefix ? `${prefix}/${item}` : item);
109831
+ } else {
109832
+ entries.push({
109833
+ relativePath: prefix ? `${prefix}/${item}` : item,
109834
+ size: stat5.size
109835
+ });
109836
+ }
109837
+ } catch {}
109838
+ }
109839
+ };
109840
+ walk(scriptsRoot, "");
109841
+ if (entries.length === 0) {
109842
+ context.emit("text", `
109843
+ No generated files yet.
109844
+ `);
109845
+ context.emit("done");
109846
+ return { handled: true };
109847
+ }
109848
+ const formatSize2 = (bytes) => {
109849
+ if (bytes < 1024)
109850
+ return `${bytes} B`;
109851
+ if (bytes < 1048576)
109852
+ return `${(bytes / 1024).toFixed(1)} KB`;
109853
+ return `${(bytes / 1048576).toFixed(1)} MB`;
109854
+ };
109855
+ let message = `
109856
+ **Generated Files** (${entries.length})
109857
+ `;
109858
+ message += `\uD83D\uDCC2 ${scriptsRoot}
109859
+
109860
+ `;
109861
+ for (const entry of entries) {
109862
+ message += ` ${entry.relativePath} (${formatSize2(entry.size)})
109863
+ `;
109864
+ }
109865
+ message += `
109866
+ `;
109867
+ context.emit("text", message);
109868
+ context.emit("done");
109869
+ return { handled: true };
109870
+ }
109871
+ };
109872
+ }
109576
109873
  undoCommand() {
109577
109874
  return {
109578
109875
  name: "undo",
@@ -109673,7 +109970,7 @@ Not a git repository or git not available.
109673
109970
  context.setProjectContext(projectContext);
109674
109971
  }
109675
109972
  }
109676
- var VERSION2 = "1.1.78";
109973
+ var VERSION2 = "1.1.80";
109677
109974
  var init_builtin = __esm(async () => {
109678
109975
  init_src2();
109679
109976
  init_context3();
@@ -114884,7 +115181,35 @@ function findRecoverableSessions(staleThresholdMs = 120000, maxAgeMs = 24 * 60 *
114884
115181
  timestamp: row.timestamp
114885
115182
  };
114886
115183
  let messageCount = 0;
115184
+ let lastMessage = null;
115185
+ let model = null;
115186
+ let label = null;
114887
115187
  const cwd = context.cwd || process.cwd();
115188
+ try {
115189
+ const persisted = db.query("SELECT label, assistant_id FROM persisted_sessions WHERE id = ?").get(row.session_id);
115190
+ if (persisted?.label) {
115191
+ label = persisted.label;
115192
+ }
115193
+ if (persisted?.assistant_id) {
115194
+ try {
115195
+ const assistant = db.query("SELECT model FROM assistants_config WHERE id = ?").get(persisted.assistant_id);
115196
+ if (assistant?.model) {
115197
+ model = assistant.model;
115198
+ }
115199
+ } catch {}
115200
+ }
115201
+ } catch {}
115202
+ try {
115203
+ const countRow = db.query("SELECT COUNT(*) as cnt FROM session_messages WHERE session_id = ?").get(row.session_id);
115204
+ if (countRow) {
115205
+ messageCount = countRow.cnt;
115206
+ }
115207
+ const lastMsgRow = db.query("SELECT content FROM session_messages WHERE session_id = ? AND role = ? ORDER BY timestamp DESC LIMIT 1").get(row.session_id, "user");
115208
+ if (lastMsgRow?.content) {
115209
+ const text5 = lastMsgRow.content.trim();
115210
+ lastMessage = text5.length > 80 ? text5.slice(0, 77) + "..." : text5;
115211
+ }
115212
+ } catch {}
114888
115213
  recoverableSessions.push({
114889
115214
  sessionId: row.session_id,
114890
115215
  heartbeat,
@@ -114892,7 +115217,10 @@ function findRecoverableSessions(staleThresholdMs = 120000, maxAgeMs = 24 * 60 *
114892
115217
  sessionPath: "",
114893
115218
  cwd,
114894
115219
  lastActivity: new Date(heartbeat.lastActivity || heartbeat.timestamp),
114895
- messageCount
115220
+ messageCount,
115221
+ lastMessage,
115222
+ model,
115223
+ label
114896
115224
  });
114897
115225
  } catch {
114898
115226
  continue;
@@ -195423,7 +195751,8 @@ class EmbeddedClient {
195423
195751
  success: !result.isError,
195424
195752
  resultLength: result.content.length
195425
195753
  });
195426
- }
195754
+ },
195755
+ onSessionLabel: options2?.onSessionLabel
195427
195756
  });
195428
195757
  }
195429
195758
  async initialize() {
@@ -200965,6 +201294,38 @@ var init_contacts = __esm(async () => {
200965
201294
  ]);
200966
201295
  });
200967
201296
 
201297
+ // packages/core/src/sessions/auto-name.ts
201298
+ async function generateSessionName(userMessage, options2 = {}) {
201299
+ const apiKey = options2.apiKey || process.env.ANTHROPIC_API_KEY;
201300
+ if (!apiKey) {
201301
+ throw new Error("ANTHROPIC_API_KEY required for session auto-naming");
201302
+ }
201303
+ const client = new Anthropic({ apiKey });
201304
+ const model = options2.model || DEFAULT_BACKGROUND_MODEL;
201305
+ const truncated = userMessage.length > 500 ? userMessage.slice(0, 500) + "..." : userMessage;
201306
+ const response = await client.messages.create({
201307
+ model,
201308
+ max_tokens: 30,
201309
+ messages: [
201310
+ {
201311
+ role: "user",
201312
+ content: `Generate a 3-5 word title for this conversation. User asked: ${truncated}
201313
+
201314
+ Reply with ONLY the title, no quotes.`
201315
+ }
201316
+ ]
201317
+ });
201318
+ const block = response.content[0];
201319
+ if (block.type === "text") {
201320
+ return block.text.replace(/^["']|["']$/g, "").trim();
201321
+ }
201322
+ return "Untitled Session";
201323
+ }
201324
+ var DEFAULT_BACKGROUND_MODEL = "claude-haiku-4-5-20251001";
201325
+ var init_auto_name = __esm(() => {
201326
+ init_sdk();
201327
+ });
201328
+
200968
201329
  // packages/core/src/sessions/store.ts
200969
201330
  function rowToSession2(row) {
200970
201331
  return {
@@ -201062,7 +201423,15 @@ class SessionRegistry {
201062
201423
  assistantId: options2.assistantId,
201063
201424
  backend: options2.backend,
201064
201425
  basePath: this.basePath,
201065
- workspaceId: this.workspaceId ?? undefined
201426
+ workspaceId: this.workspaceId ?? undefined,
201427
+ onSessionLabel: (sessionId, label) => {
201428
+ const session = this.sessions.get(sessionId);
201429
+ if (session && !session.label) {
201430
+ session.label = label;
201431
+ session.updatedAt = Date.now();
201432
+ this.persistSession(session);
201433
+ }
201434
+ }
201066
201435
  };
201067
201436
  const client = this.clientFactory(options2.cwd, clientOptions);
201068
201437
  await client.initialize();
@@ -201595,6 +201964,7 @@ var init_tools15 = __esm(() => {
201595
201964
 
201596
201965
  // packages/core/src/sessions/index.ts
201597
201966
  var init_sessions3 = __esm(async () => {
201967
+ init_auto_name();
201598
201968
  init_tools15();
201599
201969
  await __promiseAll([
201600
201970
  init_verification(),
@@ -210611,6 +210981,7 @@ var init_loop = __esm(async () => {
210611
210981
  init_limits();
210612
210982
  init_llm_response();
210613
210983
  init_manager3();
210984
+ init_auto_name();
210614
210985
  init_self_awareness();
210615
210986
  init_memory2();
210616
210987
  init_agents();
@@ -210772,6 +211143,8 @@ var init_loop = __esm(async () => {
210772
211143
  onToolEnd;
210773
211144
  onTokenUsage;
210774
211145
  onBudgetWarning;
211146
+ onSessionLabel;
211147
+ sessionAutoNamed = false;
210775
211148
  constructor(options2 = {}) {
210776
211149
  this.storageDir = options2.storageDir ?? getConfigDir();
210777
211150
  this.workspaceId = options2.workspaceId ?? null;
@@ -210805,6 +211178,7 @@ var init_loop = __esm(async () => {
210805
211178
  this.onToolEnd = options2.onToolEnd;
210806
211179
  this.onTokenUsage = options2.onTokenUsage;
210807
211180
  this.onBudgetWarning = options2.onBudgetWarning;
211181
+ this.onSessionLabel = options2.onSessionLabel;
210808
211182
  this.budgetConfig = options2.budgetConfig || null;
210809
211183
  this.guardrailsConfig = options2.guardrailsConfig || null;
210810
211184
  this.onGuardrailsViolation = options2.onGuardrailsViolation;
@@ -210928,7 +211302,7 @@ var init_loop = __esm(async () => {
210928
211302
  this.contextManager = new ContextManager(this.contextConfig, summarizer, tokenCounter);
210929
211303
  }
210930
211304
  this.toolRegistry.register(BashTool.tool, BashTool.executor);
210931
- FilesystemTools.registerAll(this.toolRegistry, this.sessionId);
211305
+ FilesystemTools.registerAll(this.toolRegistry, this.sessionId, this.config.workspace);
210932
211306
  WebTools.registerAll(this.toolRegistry);
210933
211307
  ImageTools.registerAll(this.toolRegistry);
210934
211308
  AudioTools.registerAll(this.toolRegistry);
@@ -211465,6 +211839,13 @@ You are running in **autonomous mode**. You manage your own wakeup schedule.
211465
211839
  const messages = this.context.getMessages().slice(beforeCount);
211466
211840
  const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
211467
211841
  const summary = lastAssistant?.content?.trim();
211842
+ if (!this.sessionAutoNamed && this.onSessionLabel && source === "user") {
211843
+ this.sessionAutoNamed = true;
211844
+ const bgModel = this.config?.backgroundModel;
211845
+ generateSessionName(userMessage, { model: bgModel }).then((label) => {
211846
+ this.onSessionLabel?.(this.sessionId, label);
211847
+ }).catch(() => {});
211848
+ }
211468
211849
  return { ok: true, summary: summary ? summary.slice(0, 200) : undefined };
211469
211850
  } catch (error4) {
211470
211851
  const message = error4 instanceof Error ? error4.message : String(error4);
@@ -215367,6 +215748,7 @@ __export(exports_src3, {
215367
215748
  getActiveWorkspaceId: () => getActiveWorkspaceId,
215368
215749
  generateWebhookSecret: () => generateWebhookSecret,
215369
215750
  generateWebhookId: () => generateWebhookId,
215751
+ generateSessionName: () => generateSessionName,
215370
215752
  generateEventId: () => generateEventId,
215371
215753
  generateDeliveryId: () => generateDeliveryId,
215372
215754
  formatRelativeTime: () => formatRelativeTime,
@@ -215728,6 +216110,7 @@ var init_src3 = __esm(async () => {
215728
216110
  init_anthropic();
215729
216111
  init_openai2();
215730
216112
  init_models2();
216113
+ init_auto_name();
215731
216114
  await __promiseAll([
215732
216115
  init_runtime(),
215733
216116
  init_database(),
@@ -260823,6 +261206,7 @@ function SessionSelector({
260823
261206
  const time3 = formatSessionTime(session.updatedAt);
260824
261207
  const path7 = formatPath(session.cwd);
260825
261208
  const processing = session.isProcessing ? " (processing)" : "";
261209
+ const displayName = session.label || path7;
260826
261210
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
260827
261211
  children: /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
260828
261212
  inverse: isSelected,
@@ -260835,7 +261219,7 @@ function SessionSelector({
260835
261219
  ". ",
260836
261220
  time3,
260837
261221
  " ",
260838
- path7,
261222
+ displayName,
260839
261223
  processing
260840
261224
  ]
260841
261225
  }, undefined, true, undefined, this)
@@ -261749,32 +262133,51 @@ function RecoveryPanel({ sessions, onRecover, onStartFresh }) {
261749
262133
  }, undefined, false, undefined, this),
261750
262134
  visibleSessions.map(({ session, originalIndex }) => {
261751
262135
  const isSelected = originalIndex === selectedSessionIndex;
261752
- const stateLabel = getStateLabel(session.heartbeat.state);
261753
262136
  const timeAgo = formatTimeAgo3(session.lastActivity);
261754
262137
  const cwdDisplay = truncatePath(session.cwd, 30);
261755
- const msgCount = session.messageCount > 0 ? ` ${session.messageCount} msgs` : "";
262138
+ const displayName = session.label || cwdDisplay;
262139
+ const msgCount = session.messageCount > 0 ? `${session.messageCount} msgs` : "";
262140
+ const modelName = session.model || "";
262141
+ const meta = [msgCount, modelName].filter(Boolean).join(", ");
261756
262142
  return /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Box_default, {
262143
+ flexDirection: "column",
261757
262144
  paddingY: 0,
261758
- children: /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261759
- inverse: isSelected,
261760
- children: [
261761
- isSelected ? "\u25B6" : " ",
261762
- " ",
261763
- cwdDisplay,
261764
- msgCount,
261765
- /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261766
- dimColor: true,
261767
- children: [
261768
- " (",
261769
- stateLabel,
261770
- ", ",
261771
- timeAgo,
261772
- ")"
261773
- ]
261774
- }, undefined, true, undefined, this)
261775
- ]
261776
- }, undefined, true, undefined, this)
261777
- }, session.sessionId, false, undefined, this);
262145
+ children: [
262146
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262147
+ inverse: isSelected,
262148
+ children: [
262149
+ isSelected ? "\u25B6" : " ",
262150
+ " ",
262151
+ displayName,
262152
+ " ",
262153
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262154
+ dimColor: true,
262155
+ children: [
262156
+ "(",
262157
+ timeAgo,
262158
+ ")"
262159
+ ]
262160
+ }, undefined, true, undefined, this),
262161
+ meta ? /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262162
+ dimColor: true,
262163
+ children: [
262164
+ " \u2014 ",
262165
+ meta
262166
+ ]
262167
+ }, undefined, true, undefined, this) : null
262168
+ ]
262169
+ }, undefined, true, undefined, this),
262170
+ session.lastMessage && /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262171
+ dimColor: true,
262172
+ children: [
262173
+ " ",
262174
+ "\u2018",
262175
+ session.lastMessage,
262176
+ "\u2019"
262177
+ ]
262178
+ }, undefined, true, undefined, this)
262179
+ ]
262180
+ }, session.sessionId, true, undefined, this);
261778
262181
  }),
261779
262182
  showDownArrow && /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Box_default, {
261780
262183
  children: /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
@@ -261788,33 +262191,49 @@ function RecoveryPanel({ sessions, onRecover, onStartFresh }) {
261788
262191
  }, undefined, false, undefined, this)
261789
262192
  ]
261790
262193
  }, undefined, true, undefined, this),
261791
- selectedSessionIndex >= 0 && selectedSessionIndex < sessions.length && /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Box_default, {
261792
- flexDirection: "column",
261793
- marginBottom: 1,
261794
- children: [
261795
- /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261796
- dimColor: true,
261797
- children: "Selected:"
261798
- }, undefined, false, undefined, this),
261799
- /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261800
- children: [
261801
- " ",
261802
- /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261803
- color: "cyan",
261804
- children: sessions[selectedSessionIndex].cwd
261805
- }, undefined, false, undefined, this)
261806
- ]
261807
- }, undefined, true, undefined, this),
261808
- /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261809
- children: [
261810
- " ",
261811
- formatTimeAgo3(sessions[selectedSessionIndex].lastActivity),
261812
- " \xB7 ",
261813
- getStateLabel(sessions[selectedSessionIndex].heartbeat.state)
261814
- ]
261815
- }, undefined, true, undefined, this)
261816
- ]
261817
- }, undefined, true, undefined, this),
262194
+ selectedSessionIndex >= 0 && selectedSessionIndex < sessions.length && (() => {
262195
+ const s5 = sessions[selectedSessionIndex];
262196
+ const details = [
262197
+ formatTimeAgo3(s5.lastActivity),
262198
+ getStateLabel(s5.heartbeat.state),
262199
+ s5.messageCount > 0 ? `${s5.messageCount} messages` : null,
262200
+ s5.model || null
262201
+ ].filter(Boolean).join(" \xB7 ");
262202
+ return /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Box_default, {
262203
+ flexDirection: "column",
262204
+ marginBottom: 1,
262205
+ children: [
262206
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262207
+ dimColor: true,
262208
+ children: "Selected:"
262209
+ }, undefined, false, undefined, this),
262210
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262211
+ children: [
262212
+ " ",
262213
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262214
+ color: "cyan",
262215
+ children: s5.cwd
262216
+ }, undefined, false, undefined, this)
262217
+ ]
262218
+ }, undefined, true, undefined, this),
262219
+ /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262220
+ children: [
262221
+ " ",
262222
+ details
262223
+ ]
262224
+ }, undefined, true, undefined, this),
262225
+ s5.lastMessage && /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
262226
+ dimColor: true,
262227
+ children: [
262228
+ " Last: ",
262229
+ "\u2018",
262230
+ s5.lastMessage,
262231
+ "\u2019"
262232
+ ]
262233
+ }, undefined, true, undefined, this)
262234
+ ]
262235
+ }, undefined, true, undefined, this);
262236
+ })(),
261818
262237
  /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Box_default, {
261819
262238
  children: /* @__PURE__ */ jsx_dev_runtime14.jsxDEV(Text, {
261820
262239
  dimColor: true,
@@ -292705,7 +293124,8 @@ ${msg.body || msg.preview}`);
292705
293124
  const isThinking = isProcessing && !currentResponse && !currentToolCall && toolCallEntries.length === 0;
292706
293125
  return /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
292707
293126
  flexDirection: "column",
292708
- padding: 1,
293127
+ height: rows,
293128
+ paddingX: 1,
292709
293129
  children: [
292710
293130
  showWelcome && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(WelcomeBanner, {
292711
293131
  version: version4 ?? "unknown",
@@ -292724,45 +293144,52 @@ ${msg.body || msg.preview}`);
292724
293144
  ]
292725
293145
  }, undefined, true, undefined, this)
292726
293146
  }, undefined, false, undefined, this),
292727
- /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Static, {
292728
- items: staticMessages,
292729
- children: (message) => /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Messages5, {
292730
- messages: [message],
292731
- currentResponse: undefined,
292732
- streamingMessages: [],
292733
- currentToolCall: undefined,
292734
- lastToolResult: undefined,
292735
- activityLog: [],
292736
- queuedMessageIds,
292737
- verboseTools
292738
- }, message.id, false, undefined, this)
292739
- }, staticResetKey, false, undefined, this),
292740
- showDynamicPanel && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(jsx_dev_runtime51.Fragment, {
293147
+ /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
293148
+ flexDirection: "column",
293149
+ flexGrow: 1,
293150
+ overflowY: "hidden",
292741
293151
  children: [
292742
- isProcessing && streamingTrimmed && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
292743
- marginBottom: 1,
292744
- children: /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Text, {
292745
- dimColor: true,
292746
- children: "\u22EF showing latest output"
292747
- }, undefined, false, undefined, this)
292748
- }, undefined, false, undefined, this),
292749
- isProcessing && activityTrim.trimmed && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
292750
- marginBottom: 1,
292751
- children: /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Text, {
292752
- dimColor: true,
292753
- children: "\u22EF showing latest activity"
292754
- }, undefined, false, undefined, this)
292755
- }, undefined, false, undefined, this),
292756
- /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Messages5, {
292757
- messages: [],
292758
- currentResponse: undefined,
292759
- streamingMessages: combinedStreamingMessages,
292760
- currentToolCall: undefined,
292761
- lastToolResult: undefined,
292762
- activityLog: isProcessing ? activityTrim.entries : [],
292763
- queuedMessageIds,
292764
- verboseTools
292765
- }, "streaming", false, undefined, this)
293152
+ /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Static, {
293153
+ items: staticMessages,
293154
+ children: (message) => /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Messages5, {
293155
+ messages: [message],
293156
+ currentResponse: undefined,
293157
+ streamingMessages: [],
293158
+ currentToolCall: undefined,
293159
+ lastToolResult: undefined,
293160
+ activityLog: [],
293161
+ queuedMessageIds,
293162
+ verboseTools
293163
+ }, message.id, false, undefined, this)
293164
+ }, staticResetKey, false, undefined, this),
293165
+ showDynamicPanel && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(jsx_dev_runtime51.Fragment, {
293166
+ children: [
293167
+ isProcessing && streamingTrimmed && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
293168
+ marginBottom: 1,
293169
+ children: /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Text, {
293170
+ dimColor: true,
293171
+ children: "\u22EF showing latest output"
293172
+ }, undefined, false, undefined, this)
293173
+ }, undefined, false, undefined, this),
293174
+ isProcessing && activityTrim.trimmed && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Box_default, {
293175
+ marginBottom: 1,
293176
+ children: /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Text, {
293177
+ dimColor: true,
293178
+ children: "\u22EF showing latest activity"
293179
+ }, undefined, false, undefined, this)
293180
+ }, undefined, false, undefined, this),
293181
+ /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(Messages5, {
293182
+ messages: [],
293183
+ currentResponse: undefined,
293184
+ streamingMessages: combinedStreamingMessages,
293185
+ currentToolCall: undefined,
293186
+ lastToolResult: undefined,
293187
+ activityLog: isProcessing ? activityTrim.entries : [],
293188
+ queuedMessageIds,
293189
+ verboseTools
293190
+ }, "streaming", false, undefined, this)
293191
+ ]
293192
+ }, undefined, true, undefined, this)
292766
293193
  ]
292767
293194
  }, undefined, true, undefined, this),
292768
293195
  askUserState && activeAskQuestion && !interviewState && /* @__PURE__ */ jsx_dev_runtime51.jsxDEV(AskUserPanel, {
@@ -293335,7 +293762,7 @@ process.on("unhandledRejection", (reason) => {
293335
293762
  cleanup();
293336
293763
  process.exit(1);
293337
293764
  });
293338
- var VERSION4 = "1.1.78";
293765
+ var VERSION4 = "1.1.80";
293339
293766
  var SYNC_START = "\x1B[?2026h";
293340
293767
  var SYNC_END = "\x1B[?2026l";
293341
293768
  function enableSynchronizedOutput() {
@@ -293480,4 +293907,4 @@ export {
293480
293907
  main
293481
293908
  };
293482
293909
 
293483
- //# debugId=A2CC81C0C793744C64756E2164756E21
293910
+ //# debugId=5648E49F1CE1903A64756E2164756E21