@coffer-org/plugin-claude-agent 1.4.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.
@@ -0,0 +1,45 @@
1
+ const defaultTimer = (cb, ms) => {
2
+ const t = setTimeout(cb, ms);
3
+ if (typeof t.unref === 'function')
4
+ t.unref();
5
+ };
6
+ export function makeScheduler(run, opts = {}) {
7
+ const delayMs = opts.delayMs ?? 500;
8
+ const setTimer = opts.setTimer ?? defaultTimer;
9
+ let armed = false;
10
+ let running = false;
11
+ let pending = false;
12
+ async function fire() {
13
+ armed = false;
14
+ if (running) {
15
+ pending = true;
16
+ return;
17
+ }
18
+ running = true;
19
+ try {
20
+ do {
21
+ pending = false;
22
+ try {
23
+ await run();
24
+ }
25
+ catch {
26
+ }
27
+ } while (pending);
28
+ }
29
+ finally {
30
+ running = false;
31
+ }
32
+ }
33
+ return {
34
+ schedule() {
35
+ if (running) {
36
+ pending = true;
37
+ return;
38
+ }
39
+ if (armed)
40
+ return;
41
+ armed = true;
42
+ setTimer(() => void fire(), delayMs);
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
2
+ import type { EmbeddingHit } from '@coffer-org/server/embeddings';
3
+ export declare function formatHits(hits: EmbeddingHit[]): string;
4
+ export declare function makeRagServer(cfg: {
5
+ embeddingApiKey: string;
6
+ ragTopK: number;
7
+ }): McpServerConfig;
@@ -0,0 +1,38 @@
1
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
2
+ import { z } from 'zod';
3
+ import { searchEmbeddings } from '@coffer-org/server/embeddings';
4
+ import { embedOne, EMBED_MODEL } from "./embed.js";
5
+ import { isTracing, flushTracing, langfuseFactory } from "./tracing.js";
6
+ export function formatHits(hits) {
7
+ if (hits.length === 0)
8
+ return 'No matching records.';
9
+ return hits
10
+ .map((h) => `[${h.type}/${h.recordId}] (dist ${h.distance.toFixed(3)})\n${h.snippet}`)
11
+ .join('\n\n');
12
+ }
13
+ export function makeRagServer(cfg) {
14
+ return createSdkMcpServer({
15
+ name: 'rag',
16
+ version: '1.0.0',
17
+ tools: [
18
+ tool('search_records', "Semantic search over the user's coffer records. Returns the most relevant records as type/id refs with a text snippet.", { query: z.string(), k: z.number().int().positive().optional() }, async (args) => {
19
+ const gen = isTracing()
20
+ ? langfuseFactory.startObservation('embed-query', { model: EMBED_MODEL, input: args.query }, { asType: 'generation' })
21
+ : undefined;
22
+ let vector;
23
+ try {
24
+ const r = await embedOne(args.query, cfg.embeddingApiKey);
25
+ vector = r.vector;
26
+ gen?.update({ usageDetails: { total: r.tokens } });
27
+ }
28
+ finally {
29
+ gen?.end();
30
+ if (gen)
31
+ await flushTracing();
32
+ }
33
+ const hits = await searchEmbeddings(vector, args.k ?? cfg.ragTopK);
34
+ return { content: [{ type: 'text', text: formatHits(hits) }] };
35
+ }),
36
+ ],
37
+ });
38
+ }
@@ -0,0 +1,73 @@
1
+ export interface TracingConfig {
2
+ tracingEnabled: boolean;
3
+ langfusePublicKey: string;
4
+ langfuseSecretKey: string;
5
+ langfuseBaseUrl: string;
6
+ }
7
+ export interface Obs {
8
+ startObservation(name: string, attrs?: Record<string, unknown>, opts?: {
9
+ asType?: string;
10
+ }): Obs;
11
+ update(attrs: Record<string, unknown>): Obs;
12
+ updateTrace(attrs: Record<string, unknown>): Obs;
13
+ end(): void;
14
+ }
15
+ export interface ObsFactory {
16
+ startObservation(name: string, attrs?: Record<string, unknown>, opts?: {
17
+ asType?: string;
18
+ }): Obs;
19
+ }
20
+ export declare function shouldEnable(cfg: TracingConfig): boolean;
21
+ export declare function initTracing(cfg: TracingConfig): void;
22
+ export declare function isTracing(): boolean;
23
+ export declare function flushTracing(): Promise<void>;
24
+ export declare const langfuseFactory: ObsFactory;
25
+ type ContentBlock = {
26
+ type: 'text';
27
+ text: string;
28
+ } | {
29
+ type: 'tool_use';
30
+ id: string;
31
+ name: string;
32
+ input: unknown;
33
+ } | {
34
+ type: 'tool_result';
35
+ tool_use_id: string;
36
+ content: unknown;
37
+ } | {
38
+ type: string;
39
+ [k: string]: unknown;
40
+ };
41
+ interface Usage {
42
+ input_tokens?: number;
43
+ output_tokens?: number;
44
+ cache_read_input_tokens?: number;
45
+ cache_creation_input_tokens?: number;
46
+ }
47
+ export type AgentMsg = {
48
+ type: 'assistant';
49
+ message: {
50
+ model?: string;
51
+ content: ContentBlock[];
52
+ usage?: Usage;
53
+ };
54
+ } | {
55
+ type: 'user';
56
+ message: {
57
+ content: ContentBlock[];
58
+ };
59
+ } | {
60
+ type: 'result';
61
+ subtype: string;
62
+ session_id?: string;
63
+ usage?: Usage;
64
+ result?: string;
65
+ };
66
+ export declare class TurnTracer {
67
+ private root;
68
+ private tools;
69
+ constructor(factory: ObsFactory, prompt: string, resumeSessionId: string | null);
70
+ onMessage(msg: AgentMsg): void;
71
+ fail(message: string): void;
72
+ }
73
+ export {};
@@ -0,0 +1,114 @@
1
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
2
+ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import { startObservation } from '@langfuse/tracing';
4
+ export function shouldEnable(cfg) {
5
+ return Boolean(cfg.tracingEnabled && cfg.langfusePublicKey && cfg.langfuseSecretKey);
6
+ }
7
+ let provider;
8
+ let processor;
9
+ let enabled = false;
10
+ export function initTracing(cfg) {
11
+ if (provider)
12
+ return;
13
+ if (!shouldEnable(cfg))
14
+ return;
15
+ processor = new LangfuseSpanProcessor({
16
+ publicKey: cfg.langfusePublicKey,
17
+ secretKey: cfg.langfuseSecretKey,
18
+ baseUrl: cfg.langfuseBaseUrl,
19
+ });
20
+ provider = new NodeTracerProvider({ spanProcessors: [processor] });
21
+ provider.register();
22
+ enabled = true;
23
+ }
24
+ export function isTracing() {
25
+ return enabled;
26
+ }
27
+ export async function flushTracing() {
28
+ if (processor)
29
+ await processor.forceFlush();
30
+ }
31
+ export const langfuseFactory = {
32
+ startObservation: (name, attrs, opts) => startObservation(name, attrs, opts),
33
+ };
34
+ function usageDetails(u) {
35
+ if (!u)
36
+ return undefined;
37
+ const d = {};
38
+ if (u.input_tokens != null)
39
+ d.input = u.input_tokens;
40
+ if (u.output_tokens != null)
41
+ d.output = u.output_tokens;
42
+ if (u.cache_read_input_tokens != null)
43
+ d.cache_read_input_tokens = u.cache_read_input_tokens;
44
+ if (u.cache_creation_input_tokens != null)
45
+ d.cache_creation_input_tokens = u.cache_creation_input_tokens;
46
+ return Object.keys(d).length ? d : undefined;
47
+ }
48
+ export class TurnTracer {
49
+ root;
50
+ tools = new Map();
51
+ constructor(factory, prompt, resumeSessionId) {
52
+ this.root = factory.startObservation('agent-turn', { input: prompt, metadata: { resumeSessionId } }, { asType: 'agent' });
53
+ if (resumeSessionId)
54
+ this.root.updateTrace({ sessionId: resumeSessionId });
55
+ }
56
+ onMessage(msg) {
57
+ if (msg.type === 'assistant') {
58
+ let text = '';
59
+ const toolUses = [];
60
+ for (const b of msg.message.content) {
61
+ if (b.type === 'text')
62
+ text += b.text;
63
+ else if (b.type === 'tool_use')
64
+ toolUses.push(b);
65
+ }
66
+ const gen = this.root.startObservation('llm-call', { model: msg.message.model, output: { text, toolUses } }, { asType: 'generation' });
67
+ const ud = usageDetails(msg.message.usage);
68
+ if (ud)
69
+ gen.update({ usageDetails: ud });
70
+ gen.end();
71
+ for (const tu of toolUses) {
72
+ const t = tu;
73
+ const obs = this.root.startObservation(t.name, { input: t.input }, { asType: 'tool' });
74
+ this.tools.set(t.id, obs);
75
+ }
76
+ return;
77
+ }
78
+ if (msg.type === 'user') {
79
+ for (const b of msg.message.content) {
80
+ if (b.type === 'tool_result') {
81
+ const r = b;
82
+ const obs = this.tools.get(r.tool_use_id);
83
+ if (obs) {
84
+ obs.update({ output: r.content }).end();
85
+ this.tools.delete(r.tool_use_id);
86
+ }
87
+ }
88
+ }
89
+ return;
90
+ }
91
+ if (msg.type === 'result') {
92
+ for (const obs of this.tools.values())
93
+ obs.end();
94
+ this.tools.clear();
95
+ if (msg.session_id)
96
+ this.root.updateTrace({ sessionId: msg.session_id });
97
+ const out = { output: msg.result ?? null };
98
+ const ud = usageDetails(msg.usage);
99
+ if (ud)
100
+ out.usageDetails = ud;
101
+ if (msg.subtype !== 'success') {
102
+ out.level = 'ERROR';
103
+ out.statusMessage = `result subtype=${msg.subtype}`;
104
+ }
105
+ this.root.update(out).end();
106
+ }
107
+ }
108
+ fail(message) {
109
+ for (const obs of this.tools.values())
110
+ obs.end();
111
+ this.tools.clear();
112
+ this.root.update({ level: 'ERROR', statusMessage: message }).end();
113
+ }
114
+ }
@@ -0,0 +1,23 @@
1
+ export interface ConvMessage {
2
+ role: 'user' | 'assistant';
3
+ content: string;
4
+ sender?: string | null;
5
+ msgId: string;
6
+ ts: number;
7
+ }
8
+ export interface SystemLayer {
9
+ text: string;
10
+ stable: boolean;
11
+ }
12
+ export interface AgentRequest {
13
+ system: SystemLayer[];
14
+ messages: ConvMessage[];
15
+ onDelta?: (accumulated: string) => void;
16
+ onSegment?: () => void;
17
+ }
18
+ export interface AgentResult {
19
+ text: string | null;
20
+ tokensIn: number | null;
21
+ tokensOut: number | null;
22
+ stopReason: string | null;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { AgentConfig } from './config.ts';
2
+ interface WarnLog {
3
+ warn?: (m: string) => void;
4
+ }
5
+ export declare function linkCredentials(cfg: Pick<AgentConfig, 'claudeHomeDir' | 'globalCredentials'>, log?: WarnLog): boolean;
6
+ export declare function ensureWorkspace(cfg: Pick<AgentConfig, 'appRoot' | 'workspaceDir' | 'claudeHomeDir' | 'stateDir' | 'globalCredentials'>, log?: WarnLog): void;
7
+ export {};
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function linkCredentials(cfg, log = console) {
4
+ const target = path.join(cfg.claudeHomeDir, '.credentials.json');
5
+ fs.rmSync(target, { force: true });
6
+ if (fs.existsSync(cfg.globalCredentials)) {
7
+ fs.symlinkSync(cfg.globalCredentials, target);
8
+ return true;
9
+ }
10
+ log.warn?.(`[agent] global credentials not found (${cfg.globalCredentials}) — claude will be "Not logged in"`);
11
+ return false;
12
+ }
13
+ export function ensureWorkspace(cfg, log = console) {
14
+ fs.mkdirSync(path.join(cfg.stateDir, '.sessions'), { recursive: true });
15
+ fs.mkdirSync(path.join(cfg.workspaceDir, '.claude', 'skills'), { recursive: true });
16
+ fs.mkdirSync(cfg.claudeHomeDir, { recursive: true });
17
+ const mcp = { mcpServers: {} };
18
+ fs.writeFileSync(path.join(cfg.workspaceDir, '.mcp.json'), JSON.stringify(mcp, null, 2) + '\n', 'utf-8');
19
+ const appGlob = `//${cfg.appRoot.replace(/^\/+/, '')}/**`;
20
+ const settings = {
21
+ permissions: {
22
+ allow: ['mcp__coffer__*', 'WebSearch', 'WebFetch', 'Skill'],
23
+ deny: ['Bash', 'Edit', 'Write', `Read(${appGlob})`, `Glob(${appGlob})`, `Grep(${appGlob})`],
24
+ },
25
+ enableAllProjectMcpServers: false,
26
+ };
27
+ fs.writeFileSync(path.join(cfg.claudeHomeDir, 'settings.json'), JSON.stringify(settings, null, 2) + '\n', 'utf-8');
28
+ linkCredentials(cfg, log);
29
+ }