@coffer-org/plugin-claude-agent 2.4.0 → 2.5.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.
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import Anthropic from '@anthropic-ai/sdk';
3
3
  import { effortParam, loadAgentConfig, modelId } from "./config.js";
4
4
  import { makeAgentTools } from "./coffer.js";
5
+ import { isAgentToolContentResult } from "./inspect-attachment.js";
5
6
  import { buildDomainSections } from '@coffer-org/server/mcp-tools';
6
7
  import { isTracing, flushTracing, langfuseFactory, TurnTracer } from "./tracing.js";
7
8
  import { getLogger } from '@coffer-org/sdk/logger';
@@ -63,7 +64,12 @@ export function renderContent(m) {
63
64
  if (m.role !== 'user')
64
65
  return m.content;
65
66
  const author = m.sender ? `<author>${esc(m.sender)}</author>\n` : '';
66
- return `${author}<body>${esc(m.content)}</body>`;
67
+ const attachments = m.attachments?.length
68
+ ? `\n<attachments>\n${m.attachments
69
+ .map((a) => `- name=${esc(a.name)} mime=${esc(a.mime ?? 'unknown')} size=${a.size ?? 'unknown'}${a.label ? ` label=${esc(a.label)}` : ''}`)
70
+ .join('\n')}\n</attachments>`
71
+ : '';
72
+ return `${author}<body>${esc(m.content)}${attachments}</body>`;
67
73
  }
68
74
  export const MAX_TOOL_RESULT_CHARS = 100_000;
69
75
  export function capToolResult(text) {
@@ -129,7 +135,8 @@ export async function runAgent(request, deps = {}) {
129
135
  try {
130
136
  const client = makeClientFn(cfg);
131
137
  const rag = cfg.ragEnabled && cfg.embeddingApiKey ? { embeddingApiKey: cfg.embeddingApiKey, topK: cfg.ragTopK } : null;
132
- const { tools, handlers } = await makeAgentToolsFn(rag);
138
+ const attachmentRefs = request.messages.flatMap((m) => m.attachments ?? []);
139
+ const { tools, handlers } = await makeAgentToolsFn(rag, attachmentRefs);
133
140
  const system = request.system.map((l) => ({
134
141
  type: 'text',
135
142
  text: l.text,
@@ -244,7 +251,17 @@ export async function runAgent(request, deps = {}) {
244
251
  }
245
252
  else {
246
253
  try {
247
- content = JSON.stringify((await handler(b.input)) ?? null);
254
+ const toolValue = await handler(b.input);
255
+ if (isAgentToolContentResult(toolValue)) {
256
+ results.push({
257
+ type: 'tool_result',
258
+ tool_use_id: b.id,
259
+ content: toolValue.content,
260
+ ...(toolValue.isError ? { is_error: true } : {}),
261
+ });
262
+ continue;
263
+ }
264
+ content = JSON.stringify(toolValue ?? null);
248
265
  }
249
266
  catch (err) {
250
267
  content = `Tool error: ${err instanceof Error ? err.message : String(err)}`;
@@ -1,11 +1,12 @@
1
1
  import { type RagDeps } from '@coffer-org/server/mcp-tools';
2
+ import type { AttachmentRef } from './types.ts';
2
3
  export interface AgentTool {
3
4
  name: string;
4
5
  description: string;
5
6
  input_schema: Record<string, unknown>;
6
7
  }
7
8
  export type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
8
- export declare function makeAgentTools(rag: RagDeps | null): Promise<{
9
+ export declare function makeAgentTools(rag: RagDeps | null, attachments?: AttachmentRef[]): Promise<{
9
10
  tools: AgentTool[];
10
11
  handlers: Record<string, ToolHandler>;
11
12
  }>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { collectMcpTools } from '@coffer-org/server/mcp-tools';
3
- export async function makeAgentTools(rag) {
3
+ import { inspectAttachment } from "./inspect-attachment.js";
4
+ export async function makeAgentTools(rag, attachments = []) {
4
5
  const defs = await collectMcpTools({ rag });
5
6
  const tools = [];
6
7
  const handlers = {};
@@ -13,5 +14,14 @@ export async function makeAgentTools(rag) {
13
14
  });
14
15
  handlers[name] = (args) => d.handler(args);
15
16
  }
17
+ if (attachments.length) {
18
+ const name = 'mcp__coffer__inspect_attachment';
19
+ tools.push({
20
+ name,
21
+ description: 'Inspect an uploaded attachment from the current conversation. This is expensive and exposes file content to the model; call it only when the user asks what is in the file or when seeing the image is necessary. For simply assigning the file to a record, use its name without this tool.',
22
+ input_schema: z.toJSONSchema(z.object({ name: z.string().describe('Exact server-generated attachment name') })),
23
+ });
24
+ handlers[name] = async (args) => inspectAttachment(String(args['name'] ?? ''), attachments);
25
+ }
16
26
  return { tools, handlers };
17
27
  }
@@ -1,5 +1,5 @@
1
1
  import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
2
2
  export { runAgent, agentSystemBase } from './agent.ts';
3
3
  export type { AgentRunDeps, AgentTracer } from './agent.ts';
4
- export type { ConvMessage, SystemLayer, AgentRequest, AgentResult } from './types.ts';
4
+ export type { AttachmentRef, ConvMessage, SystemLayer, AgentRequest, AgentResult } from './types.ts';
5
5
  export declare const serverHooks: PluginHooks;
@@ -0,0 +1,27 @@
1
+ import type { AttachmentRef } from './types.ts';
2
+ type ContentBlock = {
3
+ type: 'text';
4
+ text: string;
5
+ } | {
6
+ type: 'image';
7
+ source: {
8
+ type: 'base64';
9
+ media_type: string;
10
+ data: string;
11
+ };
12
+ } | {
13
+ type: 'document';
14
+ source: {
15
+ type: 'base64';
16
+ media_type: 'application/pdf';
17
+ data: string;
18
+ };
19
+ };
20
+ export interface AgentToolContentResult {
21
+ __cofferToolContent: true;
22
+ content: ContentBlock[];
23
+ isError?: boolean;
24
+ }
25
+ export declare function isAgentToolContentResult(value: unknown): value is AgentToolContentResult;
26
+ export declare function inspectAttachment(name: string, attachments: AttachmentRef[]): Promise<AgentToolContentResult>;
27
+ export {};
@@ -0,0 +1,65 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { mimeForName } from '@coffer-org/server/file-fields';
4
+ import { uploadsDir } from '@coffer-org/server/uploads';
5
+ const MAX_VISION_BYTES = 5 * 1024 * 1024;
6
+ const MAX_DOCUMENT_BYTES = 10 * 1024 * 1024;
7
+ const MAX_TEXT_BYTES = 512 * 1024;
8
+ export function isAgentToolContentResult(value) {
9
+ return typeof value === 'object' && value !== null && value.__cofferToolContent === true;
10
+ }
11
+ const textResult = (text, isError = false) => ({
12
+ __cofferToolContent: true,
13
+ content: [{ type: 'text', text }],
14
+ ...(isError ? { isError: true } : {}),
15
+ });
16
+ export async function inspectAttachment(name, attachments) {
17
+ const ref = attachments.find((a) => a.name === name);
18
+ if (!ref)
19
+ return textResult(`Attachment is not available in this conversation: ${name}`, true);
20
+ if (basename(name) !== name)
21
+ return textResult('Invalid attachment name.', true);
22
+ const file = join(uploadsDir(), name);
23
+ let size;
24
+ try {
25
+ size = (await stat(file)).size;
26
+ }
27
+ catch {
28
+ return textResult(`Attachment is no longer present on the server: ${name}`, true);
29
+ }
30
+ const mime = ref.mime ?? mimeForName(name);
31
+ if (mime === 'image/jpeg' || mime === 'image/png' || mime === 'image/gif' || mime === 'image/webp') {
32
+ if (size > MAX_VISION_BYTES) {
33
+ return textResult(`Image is ${size} bytes, above the ${MAX_VISION_BYTES}-byte inspection limit. It can still be attached to a record.`, true);
34
+ }
35
+ const bytes = await readFile(file);
36
+ return {
37
+ __cofferToolContent: true,
38
+ content: [
39
+ { type: 'text', text: `Attached image: ${ref.label ?? name} (${mime}, ${size} bytes).` },
40
+ { type: 'image', source: { type: 'base64', media_type: mime, data: bytes.toString('base64') } },
41
+ ],
42
+ };
43
+ }
44
+ if (mime === 'application/pdf') {
45
+ if (size > MAX_DOCUMENT_BYTES) {
46
+ return textResult(`PDF is ${size} bytes, above the ${MAX_DOCUMENT_BYTES}-byte inspection limit.`, true);
47
+ }
48
+ const bytes = await readFile(file);
49
+ return {
50
+ __cofferToolContent: true,
51
+ content: [
52
+ { type: 'text', text: `Attached PDF: ${ref.label ?? name} (${size} bytes).` },
53
+ { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: bytes.toString('base64') } },
54
+ ],
55
+ };
56
+ }
57
+ if (mime?.startsWith('text/') || mime === 'application/json' || mime === 'text/csv') {
58
+ if (size > MAX_TEXT_BYTES)
59
+ return textResult(`Text attachment is too large to inspect: ${size} bytes.`, true);
60
+ const text = (await readFile(file)).toString('utf8');
61
+ return textResult(`Attached text file: ${ref.label ?? name}\n\n${text}`);
62
+ }
63
+ return textResult(`This attachment type cannot be inspected by the agent (${mime ?? 'unknown'}). ` +
64
+ 'It can still be assigned to a file/media field by its name.', true);
65
+ }
@@ -1,6 +1,13 @@
1
+ export interface AttachmentRef {
2
+ name: string;
3
+ mime?: string;
4
+ size?: number;
5
+ label?: string;
6
+ }
1
7
  export interface ConvMessage {
2
8
  role: 'user' | 'assistant';
3
9
  content: string;
10
+ attachments?: AttachmentRef[];
4
11
  sender?: string | null;
5
12
  msgId: string;
6
13
  ts: number;
package/dist/schema.js CHANGED
@@ -4391,11 +4391,11 @@ function resolveUnits(u) {
4391
4391
  //#endregion
4392
4392
  //#region ../sdk/src/currencies.ts
4393
4393
  /**
4394
- * Валюти з вбудованого `Intl` (ISO 4217) БЕЗ hand-list і без npm-залежності.
4395
- * Коди `Intl.supportedValuesOf('currency')`; символи/назви локалізовані через
4394
+ * Currencies from built-in `Intl` (ISO 4217), with NO hand list or npm dependency.
4395
+ * Codes come from `Intl.supportedValuesOf('currency')`; symbols/names are localized through
4396
4396
  * `Intl.NumberFormat`/`Intl.DisplayNames`.
4397
4397
  */
4398
- /** Усі ISO 4217 коди. Fallback кілька основних (на випадок старого рантайму). */
4398
+ /** All ISO 4217 codes. Fallback: several major codes for older runtimes. */
4399
4399
  var CURRENCY_CODES = (() => {
4400
4400
  try {
4401
4401
  return Intl.supportedValuesOf("currency");
@@ -4410,26 +4410,26 @@ var CURRENCY_CODES = (() => {
4410
4410
  }
4411
4411
  })();
4412
4412
  var CODE_SET = new Set(CURRENCY_CODES);
4413
- /** Чи валідний ISO 4217 код. */
4413
+ /** Whether an ISO 4217 code is valid. */
4414
4414
  var isCurrencyCode = (c) => CODE_SET.has(c);
4415
4415
  //#endregion
4416
4416
  //#region ../sdk/src/fields/validation.ts
4417
- /** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
4417
+ /** Structured message for zod: JSON {code, params}. Decoded by mutate.ts. */
4418
4418
  function vmsg(code, params) {
4419
4419
  return JSON.stringify(params ? {
4420
4420
  code,
4421
4421
  params
4422
4422
  } : { code });
4423
4423
  }
4424
- /** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
4424
+ /** v4 error-map: message for a missing value (formerly required_error). */
4425
4425
  function reqErr(code = "required") {
4426
4426
  return { error: (iss) => iss.input === void 0 ? vmsg(code) : void 0 };
4427
4427
  }
4428
- /** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
4428
+ /** v4 error-map: message for an invalid type (formerly invalid_type_error). */
4429
4429
  function typeErr(code = "invalid_type") {
4430
4430
  return { error: (iss) => iss.code === "invalid_type" ? vmsg(code) : void 0 };
4431
4431
  }
4432
- /** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
4432
+ /** v4 error-map: required + invalid_type together (formerly required_error + invalid_type_error). */
4433
4433
  function reqTypeErr() {
4434
4434
  return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
4435
4435
  }
@@ -4447,10 +4447,10 @@ function jsonValue(raw) {
4447
4447
  return raw;
4448
4448
  }
4449
4449
  /**
4450
- * Фабрика для полів, що зберігають JSON і валідуються вкладеною zod-схемою.
4451
- * Приймає нативне object/array (native form-state) АБО JSON-рядок (legacy).
4452
- * Single-parse через jsonValue → inner.safeParse → issue `code`; рядок, що не
4453
- * розпарсився, лишається рядкомкод 'json'.
4450
+ * Factory for fields that store JSON and validate it with a nested zod schema.
4451
+ * Accepts a native object/array (native form state) OR a JSON string (legacy).
4452
+ * Single parse through jsonValue → inner.safeParse → issue `code`; an unparseable
4453
+ * string remains a string code 'json'.
4454
4454
  */
4455
4455
  function jsonRefined(inner, code) {
4456
4456
  return unknown().superRefine((raw, ctx) => {
@@ -4522,16 +4522,16 @@ function normalizeOpts(rawIn) {
4522
4522
  //#endregion
4523
4523
  //#region ../sdk/src/field-presets.ts
4524
4524
  /**
4525
- * Пресети полів тонкі обгортки над базовими примітивами з fields.ts.
4525
+ * Field presets are thin wrappers around the primitives in fields.ts.
4526
4526
  *
4527
- * Кожен пресет = один kind (один віджет). Пресети не приймають `format` —
4528
- * вони самі є семантичними типами. min/max/step через `config`.
4527
+ * Each preset = one kind (one widget). Presets do not accept `format`;
4528
+ * they are semantic types themselves. min/max/step go through `config`.
4529
4529
  *
4530
- * Пресети додаються до `f` через `composeF`, який гарантує, що жоден пресет
4531
- * не перебиває примітив.
4530
+ * Presets are added to `f` through `composeF`, which guarantees no preset
4531
+ * overrides a primitive.
4532
4532
  *
4533
- * Циклічний імпорт з fields.ts безпечний: фабрики/хелпери hoisted-декларації,
4534
- * а пресети викликають їх лише в тілі своїх функцій.
4533
+ * The cyclic import from fields.ts is safe: factories/helpers are hoisted declarations,
4534
+ * and presets call them only inside their function bodies.
4535
4535
  */
4536
4536
  function email(raw) {
4537
4537
  const o = normalizeOpts(raw);
@@ -4705,9 +4705,9 @@ function link(raw) {
4705
4705
  }, o.multiple ?? false));
4706
4706
  }
4707
4707
  var TEL_RE = /^\+?[\d\s()-]{4,}$/;
4708
- /** Loose URL: будь-який scheme:// АБО host-з-крапкою (+опц. порт/шлях). Без пробілів. */
4708
+ /** Loose URL: any scheme:// OR dotted host (optional port/path). No spaces. */
4709
4709
  var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
4710
- /** CSS named colors (CSS Color Module L4) для f.colorname. */
4710
+ /** CSS named colors (CSS Color Module L4) for f.colorname. */
4711
4711
  var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
4712
4712
  "aliceblue",
4713
4713
  "antiquewhite",
@@ -4966,7 +4966,7 @@ function reminder(raw) {
4966
4966
  }
4967
4967
  var _real = real;
4968
4968
  var _int = int;
4969
- /** Відсоток 0..100 — real з rules:{min:0,max:100}. */
4969
+ /** Percentage 0..100 — real with rules:{min:0,max:100}. */
4970
4970
  function percent(o) {
4971
4971
  return _real({
4972
4972
  ...o,
@@ -4977,7 +4977,7 @@ function percent(o) {
4977
4977
  }
4978
4978
  });
4979
4979
  }
4980
- /** Рік — int з rules:{min:1900,max:2100}; межі можна перекрити через rules.min/max. */
4980
+ /** Year — int with rules:{min:1900,max:2100}; bounds can be overridden via rules.min/max. */
4981
4981
  function year(o) {
4982
4982
  return _int({
4983
4983
  ...o,
@@ -6027,13 +6027,6 @@ function wrapKey(opts, meta) {
6027
6027
  role: opts.role
6028
6028
  }
6029
6029
  };
6030
- if (opts.pinned) m = {
6031
- ...m,
6032
- hints: {
6033
- ...m.hints,
6034
- pinned: true
6035
- }
6036
- };
6037
6030
  if (opts.default !== void 0) m = {
6038
6031
  ...m,
6039
6032
  default: opts.default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-claude-agent",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@anthropic-ai/sdk": "^0.111.0",
29
- "@coffer-org/mcp": "^2.3.0",
30
- "@coffer-org/sdk": "^2.1.0",
31
- "@coffer-org/server": "^2.2.0",
29
+ "@coffer-org/mcp": "^2.3.1",
30
+ "@coffer-org/sdk": "^2.1.1",
31
+ "@coffer-org/server": "^2.3.0",
32
32
  "@langfuse/otel": "^4.6.1",
33
33
  "@langfuse/tracing": "^4.6.1",
34
34
  "@opentelemetry/sdk-trace-node": "^2.8.0",