@coffer-org/plugin-claude-agent 2.4.1 → 2.5.1

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
@@ -44,7 +44,6 @@ export default definePlugin({
44
44
  fields: [
45
45
  field.boolean({ key: 'rag_enabled', label: 'claude-agent.settings.rag_enabled', default: true }),
46
46
  field.password({ key: 'openai_api_key', label: 'claude-agent.settings.openai_api_key' }),
47
- field.int({ key: 'rag_top_k', label: 'claude-agent.settings.rag_top_k', default: 5, view: { hidden: true } }),
48
47
  ],
49
48
  }),
50
49
  field.group({
@@ -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) {
@@ -128,8 +134,9 @@ export async function runAgent(request, deps = {}) {
128
134
  const turns = [];
129
135
  try {
130
136
  const client = makeClientFn(cfg);
131
- const rag = cfg.ragEnabled && cfg.embeddingApiKey ? { embeddingApiKey: cfg.embeddingApiKey, topK: cfg.ragTopK } : null;
132
- const { tools, handlers } = await makeAgentToolsFn(rag);
137
+ const rag = cfg.ragEnabled && cfg.embeddingApiKey ? { embeddingApiKey: cfg.embeddingApiKey } : null;
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
  }
@@ -17,7 +17,6 @@ export interface AgentConfig {
17
17
  skipPermissions: boolean;
18
18
  responseTimeout: number;
19
19
  ragEnabled: boolean;
20
- ragTopK: number;
21
20
  thinkingEnabled: boolean;
22
21
  embeddingApiKey: string;
23
22
  langfusePublicKey: string;
@@ -36,7 +36,6 @@ export function loadAgentConfig(opts = {}) {
36
36
  skipPermissions: db.skip_permissions !== false,
37
37
  responseTimeout: Math.max(60, Number(env.AGENT_RESPONSE_TIMEOUT ?? db.response_timeout ?? 600) || 600),
38
38
  ragEnabled: db.rag_enabled !== false,
39
- ragTopK: 5,
40
39
  thinkingEnabled: (env.AGENT_THINKING_ENABLED ?? String(db.thinking_enabled)) === 'true'
41
40
  && supportsAdaptiveThinking(modelId(claudeModel)),
42
41
  embeddingApiKey: env.OPENAI_API_KEY ?? db.openai_api_key ?? '',
@@ -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
@@ -7164,23 +7164,14 @@ var src_default = definePlugin({
7164
7164
  field.group({
7165
7165
  label: "claude-agent.settings.groups.rag",
7166
7166
  icon: "lucide:search",
7167
- fields: [
7168
- field.boolean({
7169
- key: "rag_enabled",
7170
- label: "claude-agent.settings.rag_enabled",
7171
- default: true
7172
- }),
7173
- field.password({
7174
- key: "openai_api_key",
7175
- label: "claude-agent.settings.openai_api_key"
7176
- }),
7177
- field.int({
7178
- key: "rag_top_k",
7179
- label: "claude-agent.settings.rag_top_k",
7180
- default: 5,
7181
- view: { hidden: true }
7182
- })
7183
- ]
7167
+ fields: [field.boolean({
7168
+ key: "rag_enabled",
7169
+ label: "claude-agent.settings.rag_enabled",
7170
+ default: true
7171
+ }), field.password({
7172
+ key: "openai_api_key",
7173
+ label: "claude-agent.settings.openai_api_key"
7174
+ })]
7184
7175
  }),
7185
7176
  field.group({
7186
7177
  label: "claude-agent.settings.groups.langfuse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-claude-agent",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -28,7 +28,7 @@
28
28
  "@anthropic-ai/sdk": "^0.111.0",
29
29
  "@coffer-org/mcp": "^2.3.1",
30
30
  "@coffer-org/sdk": "^2.1.1",
31
- "@coffer-org/server": "^2.2.2",
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",