goatchain 0.0.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/cli/sdk.mjs ADDED
@@ -0,0 +1,341 @@
1
+ import path from 'node:path'
2
+
3
+ export async function loadSdk() {
4
+ try {
5
+ return await import('../dist/index.js')
6
+ }
7
+ catch (err) {
8
+ const msg = err instanceof Error ? err.message : String(err)
9
+ throw new Error(
10
+ [
11
+ 'Failed to load `./dist/index.js`.',
12
+ 'If you are running from the repo, run `pnpm -s build` first.',
13
+ `cause: ${msg}`,
14
+ ].join('\n'),
15
+ )
16
+ }
17
+ }
18
+
19
+ function applyDefaultRequestOptions(model, defaults) {
20
+ const hasSetModelId = typeof model?.setModelId === 'function'
21
+
22
+ return {
23
+ get modelId() {
24
+ return model.modelId
25
+ },
26
+ ...(hasSetModelId
27
+ ? {
28
+ setModelId(id) {
29
+ model.setModelId(id)
30
+ },
31
+ }
32
+ : {}),
33
+ async invoke(messages, options) {
34
+ return model.invoke(messages, options)
35
+ },
36
+ stream: (messagesOrRequest, options) => {
37
+ if (Array.isArray(messagesOrRequest)) {
38
+ return model.stream(messagesOrRequest, options)
39
+ }
40
+ return model.stream({ ...defaults, ...messagesOrRequest })
41
+ },
42
+ ...(typeof model?.run === 'function'
43
+ ? {
44
+ run: (req) => model.run({ ...defaults, ...req }),
45
+ }
46
+ : {}),
47
+ }
48
+ }
49
+
50
+ function createTools(sdk, workspaceCwd) {
51
+ const {
52
+ ToolRegistry,
53
+ WriteTool,
54
+ ReadTool,
55
+ EditTool,
56
+ GlobTool,
57
+ GrepTool,
58
+ WebSearchTool,
59
+ } = sdk
60
+
61
+ if (![ToolRegistry, WriteTool, ReadTool, EditTool, GlobTool, GrepTool, WebSearchTool].every(v => typeof v === 'function')) {
62
+ throw new Error(
63
+ [
64
+ 'Missing builtin tool exports from `./dist/index.js`.',
65
+ 'Run `pnpm -s build` (repo) or reinstall the package to get an up-to-date build.',
66
+ ].join('\n'),
67
+ )
68
+ }
69
+
70
+ const cacheDir = path.join(workspaceCwd, '.goatchain')
71
+
72
+ const isSubpath = (parent, child) => {
73
+ const rel = path.relative(path.resolve(parent), path.resolve(child))
74
+ if (rel === '')
75
+ return true
76
+ if (rel === '..')
77
+ return false
78
+ return !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)
79
+ }
80
+
81
+ const ensureNotInCache = (absPath) => {
82
+ if (isSubpath(cacheDir, absPath)) {
83
+ throw new Error(`Access denied: ${absPath}\nRestricted: ${cacheDir}`)
84
+ }
85
+ }
86
+
87
+ const ensureAllowedFile = (filePath) => {
88
+ const abs = path.isAbsolute(filePath) ? filePath : path.resolve(workspaceCwd, filePath)
89
+ if (!isSubpath(workspaceCwd, abs)) {
90
+ throw new Error(`Access denied: ${abs}\nAllowed directory: ${workspaceCwd}`)
91
+ }
92
+ ensureNotInCache(abs)
93
+ return abs
94
+ }
95
+
96
+ const ensureAllowedDir = (dirPath) => {
97
+ const abs = path.isAbsolute(dirPath) ? dirPath : path.resolve(workspaceCwd, dirPath)
98
+ if (!isSubpath(workspaceCwd, abs)) {
99
+ throw new Error(`Access denied: ${abs}\nAllowed directory: ${workspaceCwd}`)
100
+ }
101
+ ensureNotInCache(abs)
102
+ return abs
103
+ }
104
+
105
+ const registry = new ToolRegistry()
106
+
107
+ const writeInner = new WriteTool({ cwd: workspaceCwd, allowedDirectory: workspaceCwd })
108
+ registry.register({
109
+ name: writeInner.name,
110
+ description: writeInner.description,
111
+ parameters: writeInner.parameters,
112
+ riskLevel: writeInner.riskLevel,
113
+ execute: (args) => {
114
+ if (args && typeof args === 'object' && 'file_path' in args) {
115
+ ensureAllowedFile(args.file_path)
116
+ }
117
+ return writeInner.execute(args)
118
+ },
119
+ })
120
+
121
+ const readInner = new ReadTool({ cwd: workspaceCwd, allowedDirectory: workspaceCwd })
122
+ registry.register({
123
+ name: readInner.name,
124
+ description: readInner.description,
125
+ parameters: readInner.parameters,
126
+ riskLevel: readInner.riskLevel,
127
+ execute: (args) => {
128
+ if (args && typeof args === 'object' && 'file_path' in args) {
129
+ ensureAllowedFile(args.file_path)
130
+ }
131
+ return readInner.execute(args)
132
+ },
133
+ })
134
+
135
+ const editInner = new EditTool({ cwd: workspaceCwd })
136
+ registry.register({
137
+ name: editInner.name,
138
+ description: editInner.description,
139
+ parameters: editInner.parameters,
140
+ riskLevel: editInner.riskLevel,
141
+ execute: (args) => {
142
+ if (args && typeof args === 'object' && 'file_path' in args) {
143
+ ensureAllowedFile(args.file_path)
144
+ }
145
+ return editInner.execute(args)
146
+ },
147
+ })
148
+
149
+ const globInner = new GlobTool({ cwd: workspaceCwd })
150
+ registry.register({
151
+ name: globInner.name,
152
+ description: globInner.description,
153
+ parameters: globInner.parameters,
154
+ riskLevel: globInner.riskLevel,
155
+ execute: (args) => {
156
+ if (args && typeof args === 'object' && 'path' in args && typeof args.path === 'string' && args.path.trim()) {
157
+ ensureAllowedDir(args.path)
158
+ }
159
+ return globInner.execute(args)
160
+ },
161
+ })
162
+
163
+ const grepInner = new GrepTool({ cwd: workspaceCwd })
164
+ registry.register({
165
+ name: grepInner.name,
166
+ description: grepInner.description,
167
+ parameters: grepInner.parameters,
168
+ riskLevel: grepInner.riskLevel,
169
+ execute: (args) => {
170
+ if (args && typeof args === 'object' && 'path' in args && typeof args.path === 'string' && args.path.trim()) {
171
+ ensureAllowedDir(args.path)
172
+ }
173
+ return grepInner.execute(args)
174
+ },
175
+ })
176
+
177
+ if(process.env.SERPER_API_KEY){
178
+ const webSearchInner = new WebSearchTool({
179
+ apiKey: process.env.SERPER_API_KEY,
180
+ })
181
+ registry.register({
182
+ name: webSearchInner.name,
183
+ description: webSearchInner.description,
184
+ parameters: webSearchInner.parameters,
185
+ riskLevel: webSearchInner.riskLevel,
186
+ execute: (args) => webSearchInner.execute(args),
187
+ })
188
+ }
189
+
190
+ return { registry, workspaceCwd }
191
+ }
192
+
193
+ async function createModelClient(sdk, { getApiKey, modelId, baseUrl, requestDefaults }) {
194
+ const { createModel, createOpenAIAdapter } = sdk
195
+
196
+ if (typeof createModel === 'function' && typeof createOpenAIAdapter === 'function') {
197
+ const secretProvider = {
198
+ get: (name) => {
199
+ if (name === 'OPENAI_API_KEY')
200
+ return getApiKey()
201
+ return process.env[name]
202
+ },
203
+ }
204
+
205
+ const baseModel = createModel({
206
+ adapters: [
207
+ createOpenAIAdapter({
208
+ defaultModelId: modelId,
209
+ baseUrl,
210
+ apiKeySecretName: 'OPENAI_API_KEY',
211
+ secretProvider,
212
+ }),
213
+ ],
214
+ })
215
+ return applyDefaultRequestOptions(baseModel, requestDefaults)
216
+ }
217
+
218
+ const { default: OpenAI } = await import('openai')
219
+ let cachedClient = null
220
+ let currentModelId = modelId
221
+ let currentBaseUrl = baseUrl
222
+
223
+ const getClient = () => {
224
+ if (cachedClient)
225
+ return cachedClient
226
+ const key = getApiKey() ?? process.env.OPENAI_API_KEY
227
+ if (!key)
228
+ throw new Error('Missing OPENAI_API_KEY (required for chatting)')
229
+ cachedClient = new OpenAI({ apiKey: key, baseURL: currentBaseUrl || undefined })
230
+ return cachedClient
231
+ }
232
+
233
+ return {
234
+ get modelId() {
235
+ return currentModelId
236
+ },
237
+ setModelId(id) {
238
+ currentModelId = id
239
+ },
240
+ setBaseUrl(url) {
241
+ currentBaseUrl = url
242
+ cachedClient = null
243
+ },
244
+ resetClient() {
245
+ cachedClient = null
246
+ },
247
+ async invoke(messages) {
248
+ const client = getClient()
249
+ const resp = await client.chat.completions.create({
250
+ model: currentModelId,
251
+ messages,
252
+ stream: false,
253
+ max_completion_tokens: requestDefaults.maxOutputTokens ?? undefined,
254
+ temperature: requestDefaults.temperature ?? undefined,
255
+ top_p: requestDefaults.topP ?? undefined,
256
+ presence_penalty: requestDefaults.presencePenalty ?? undefined,
257
+ frequency_penalty: requestDefaults.frequencyPenalty ?? undefined,
258
+ seed: requestDefaults.seed ?? undefined,
259
+ })
260
+ const choice = Array.isArray(resp?.choices) ? resp.choices[0] : undefined
261
+ const content = typeof choice?.message?.content === 'string' ? choice.message.content : ''
262
+ return { message: { role: 'assistant', content } }
263
+ },
264
+ stream: async function* (messagesOrRequest, options) {
265
+ const client = getClient()
266
+ const isLegacy = Array.isArray(messagesOrRequest)
267
+ const messages = isLegacy ? messagesOrRequest : (messagesOrRequest?.messages ?? [])
268
+ const tools = isLegacy ? options?.tools : messagesOrRequest?.tools
269
+ const signal = isLegacy ? undefined : messagesOrRequest?.signal
270
+ const model = isLegacy ? currentModelId : (messagesOrRequest?.model?.modelId ?? currentModelId)
271
+
272
+ const resp = await client.chat.completions.create(
273
+ {
274
+ model,
275
+ messages,
276
+ tools,
277
+ stream: true,
278
+ stream_options: { include_usage: true },
279
+ max_completion_tokens: requestDefaults.maxOutputTokens ?? undefined,
280
+ temperature: requestDefaults.temperature ?? undefined,
281
+ top_p: requestDefaults.topP ?? undefined,
282
+ presence_penalty: requestDefaults.presencePenalty ?? undefined,
283
+ frequency_penalty: requestDefaults.frequencyPenalty ?? undefined,
284
+ seed: requestDefaults.seed ?? undefined,
285
+ },
286
+ {
287
+ signal,
288
+ timeout: typeof requestDefaults.timeoutMs === 'number' ? requestDefaults.timeoutMs : undefined,
289
+ },
290
+ )
291
+
292
+ for await (const chunk of resp) {
293
+ const delta = chunk?.choices?.[0]?.delta?.content
294
+ if (typeof delta === 'string' && delta.length > 0) {
295
+ yield { type: 'text_delta', delta }
296
+ }
297
+ const usage = chunk?.usage
298
+ if (usage && typeof usage === 'object') {
299
+ yield {
300
+ type: 'usage',
301
+ usage: {
302
+ promptTokens: usage.prompt_tokens ?? 0,
303
+ completionTokens: usage.completion_tokens ?? 0,
304
+ totalTokens: usage.total_tokens ?? 0,
305
+ },
306
+ }
307
+ }
308
+ }
309
+
310
+ yield { type: 'done' }
311
+ },
312
+ }
313
+ }
314
+
315
+ export async function createAgent({ getApiKey, modelId, system, baseUrl, requestDefaults }) {
316
+ const sdk = await loadSdk()
317
+ const { Agent } = sdk
318
+
319
+ const workspaceCwd = process.cwd()
320
+ const tools = createTools(sdk, workspaceCwd)
321
+ const model = await createModelClient(sdk, { getApiKey, modelId, baseUrl, requestDefaults })
322
+
323
+ const defaultSystemPrompt = [
324
+ 'You are a helpful assistant.',
325
+ 'If you need to inspect the local project (files, folders, code), you MUST use the provided tools (Read/Glob/Grep) and must not claim you did unless a tool was actually called.',
326
+ 'When your plan depends on inspecting the project, call the tool(s) immediately instead of stopping after saying you will.',
327
+ 'After using tools, continue and produce a user-visible result in the SAME run (don’t stop right after inspection).',
328
+ 'For coding or file-creation tasks, you MUST implement by creating/editing files in the workspace using Write/Edit, then briefly summarize what you changed and where.',
329
+ 'Write/Edit may require user approval in the CLI; still call them when needed (the CLI will prompt the user).',
330
+ 'Do NOT end your response right after saying “I will create/implement …” — either ask a necessary clarifying question or start writing files with tools.',
331
+ ].join('\n')
332
+
333
+ const agent = new Agent({
334
+ name: 'GoatChain CLI',
335
+ systemPrompt: system ?? defaultSystemPrompt,
336
+ model,
337
+ tools: tools.registry,
338
+ })
339
+
340
+ return { agent, model, tools }
341
+ }
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ function normalizeMessageContent(content) {
4
+ if (typeof content === 'string')
5
+ return content
6
+ try {
7
+ return JSON.stringify(content ?? '')
8
+ }
9
+ catch {
10
+ return String(content ?? '')
11
+ }
12
+ }
13
+
14
+ export function selectSessionsById(allSessions, ids) {
15
+ const set = new Set(Array.isArray(ids) ? ids.map(String) : [])
16
+ return (Array.isArray(allSessions) ? allSessions : []).filter(s => s && set.has(String(s.sessionId)))
17
+ }
18
+
19
+ export function buildSessionsExportBundle(sessions) {
20
+ const list = Array.isArray(sessions) ? sessions : []
21
+ return {
22
+ schemaVersion: 1,
23
+ exportedAt: Date.now(),
24
+ sessions: list.map(s => ({
25
+ sessionId: s.sessionId,
26
+ createdAt: s.createdAt,
27
+ updatedAt: s.updatedAt,
28
+ modelId: s.modelId,
29
+ systemPrompt: s.systemPrompt,
30
+ title: s.title,
31
+ summary: s.summary,
32
+ pinned: Boolean(s.pinned),
33
+ messages: Array.isArray(s.messages) ? s.messages : [],
34
+ })),
35
+ }
36
+ }
37
+
38
+ export function sessionsToMarkdown(sessions, opts = {}) {
39
+ const exportedAt = typeof opts.exportedAt === 'string' ? opts.exportedAt : new Date().toISOString()
40
+ const list = Array.isArray(sessions) ? sessions : []
41
+ const lines = []
42
+ lines.push('# Sessions export')
43
+ lines.push('')
44
+ lines.push(`exportedAt: ${exportedAt}`)
45
+ lines.push('')
46
+ for (const s of list) {
47
+ lines.push('---')
48
+ lines.push(`## ${s?.title ?? 'New session'}`)
49
+ lines.push(`- id: ${s?.sessionId ?? ''}`)
50
+ if (s?.modelId)
51
+ lines.push(`- model: ${String(s.modelId)}`)
52
+ lines.push(`- pinned: ${Boolean(s?.pinned)}`)
53
+ if (s?.summary)
54
+ lines.push(`- summary: ${String(s.summary).replace(/\n/g, ' ')}`)
55
+ lines.push('')
56
+ lines.push('### Messages')
57
+ const msgs = Array.isArray(s?.messages) ? s.messages : []
58
+ for (const m of msgs) {
59
+ const role = m?.role ? String(m.role) : 'message'
60
+ const content = normalizeMessageContent(m?.content).replace(/\n/g, '\\n')
61
+ lines.push(`- ${role}: ${content}`)
62
+ }
63
+ lines.push('')
64
+ }
65
+ return `${lines.join('\n')}\n`
66
+ }
67
+
68
+ export function parseSessionsImportJson(parsed) {
69
+ if (Array.isArray(parsed))
70
+ return parsed
71
+ if (parsed && typeof parsed === 'object') {
72
+ if (Array.isArray(parsed.sessions))
73
+ return parsed.sessions
74
+ if (typeof parsed.sessionId === 'string')
75
+ return [parsed]
76
+ }
77
+ return []
78
+ }
79
+
80
+ export async function planSessionImports(rawSessions, opts) {
81
+ const conflict = String(opts?.conflict ?? 'new_ids')
82
+ const exists = typeof opts?.exists === 'function' ? opts.exists : async () => false
83
+ const makeId = typeof opts?.makeId === 'function' ? opts.makeId : () => randomUUID()
84
+
85
+ const out = []
86
+ const list = Array.isArray(rawSessions) ? rawSessions : []
87
+ for (const s of list) {
88
+ if (!s || typeof s !== 'object')
89
+ continue
90
+ const baseId = typeof s.sessionId === 'string' && s.sessionId.trim() ? s.sessionId.trim() : makeId()
91
+ let id = baseId
92
+ // eslint-disable-next-line no-await-in-loop
93
+ const has = await exists(id)
94
+ if (has) {
95
+ if (conflict === 'skip')
96
+ continue
97
+ if (conflict === 'new_ids')
98
+ id = makeId()
99
+ }
100
+
101
+ out.push({
102
+ sessionId: id,
103
+ session: {
104
+ sessionId: id,
105
+ createdAt: typeof s.createdAt === 'number' ? s.createdAt : Date.now(),
106
+ modelId: typeof s.modelId === 'string' ? s.modelId : undefined,
107
+ systemPrompt: typeof s.systemPrompt === 'string' ? s.systemPrompt : undefined,
108
+ title: typeof s.title === 'string' ? s.title : undefined,
109
+ summary: typeof s.summary === 'string' ? s.summary : undefined,
110
+ pinned: Boolean(s.pinned),
111
+ messages: Array.isArray(s.messages) ? s.messages : [],
112
+ },
113
+ })
114
+ }
115
+
116
+ return out
117
+ }
118
+