@sulala/agent 0.1.13 → 0.1.14
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/README.md +3 -3
- package/dashboard/dist/assets/index-DegBJNv6.css +1 -0
- package/dashboard/dist/assets/index-pVHpAj3h.js +83 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +21 -6
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/skill-generate.d.ts +1 -1
- package/dist/agent/skill-generate.d.ts.map +1 -1
- package/dist/agent/skill-generate.js +10 -3
- package/dist/agent/skill-generate.js.map +1 -1
- package/dist/agent/skill-install.d.ts +12 -1
- package/dist/agent/skill-install.d.ts.map +1 -1
- package/dist/agent/skill-install.js +130 -15
- package/dist/agent/skill-install.js.map +1 -1
- package/dist/agent/skills.d.ts +4 -3
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +53 -25
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/tool/spec-loader.d.ts +7 -0
- package/dist/agent/tool/spec-loader.d.ts.map +1 -0
- package/dist/agent/tool/spec-loader.js +540 -0
- package/dist/agent/tool/spec-loader.js.map +1 -0
- package/dist/agent/tools.d.ts.map +1 -1
- package/dist/agent/tools.integrations.test.js +4 -5
- package/dist/agent/tools.integrations.test.js.map +1 -1
- package/dist/agent/tools.js +144 -367
- package/dist/agent/tools.js.map +1 -1
- package/dist/ai/orchestrator.d.ts.map +1 -1
- package/dist/ai/orchestrator.js +82 -17
- package/dist/ai/orchestrator.js.map +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -5
- package/dist/config.js.map +1 -1
- package/dist/db/index.d.ts +14 -7
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +108 -30
- package/dist/db/index.js.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +141 -15
- package/dist/gateway/server.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/onboard-env.d.ts +1 -1
- package/dist/onboard-env.d.ts.map +1 -1
- package/dist/onboard-env.js +2 -0
- package/dist/onboard-env.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher/index.d.ts.map +1 -1
- package/dist/watcher/index.js +1 -2
- package/dist/watcher/index.js.map +1 -1
- package/package.json +4 -5
- package/src/index.ts +1 -1
- package/context/00-rules.md +0 -1
- package/context/airtable.md +0 -35
- package/context/apple-notes.md +0 -99
- package/context/asana.md +0 -37
- package/context/bluesky.md +0 -46
- package/context/calendar.md +0 -63
- package/context/country-info.md +0 -13
- package/context/create-skill.md +0 -128
- package/context/discord.md +0 -30
- package/context/docs.md +0 -29
- package/context/drive.md +0 -49
- package/context/dropbox.md +0 -39
- package/context/facebook.md +0 -47
- package/context/fetch-form-api.md +0 -16
- package/context/figma.md +0 -30
- package/context/files.md +0 -30
- package/context/git.md +0 -37
- package/context/github.md +0 -58
- package/context/gmail.md +0 -52
- package/context/google.md +0 -28
- package/context/hellohub.md +0 -29
- package/context/jira.md +0 -46
- package/context/linear.md +0 -40
- package/context/news.md +0 -64
- package/context/notion.md +0 -45
- package/context/portal-integrations.md +0 -42
- package/context/post-to-x.md +0 -50
- package/context/sheets.md +0 -47
- package/context/slack.md +0 -48
- package/context/slides.md +0 -35
- package/context/stripe.md +0 -38
- package/context/tes.md +0 -7
- package/context/test.md +0 -7
- package/context/weather.md +0 -32
- package/context/zoom.md +0 -28
- package/dashboard/dist/assets/index-BTx-9jCj.css +0 -1
- package/dashboard/dist/assets/index-B_QGQ8c_.js +0 -83
- package/registry/apple-notes.md +0 -99
- package/registry/bluesky.md +0 -34
- package/registry/files.md +0 -30
- package/registry/git.md +0 -37
- package/registry/news.md +0 -64
- package/registry/skills-registry.json +0 -46
- package/registry/weather.md +0 -32
package/dist/agent/tools.js
CHANGED
|
@@ -3,12 +3,10 @@ import { resolve, relative, dirname, join } from 'path';
|
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { insertTask, getOrCreateAgentSession } from '../db/index.js';
|
|
5
5
|
import { config, getPortalGatewayBase, getEffectivePortalApiKey } from '../config.js';
|
|
6
|
-
import {
|
|
7
|
-
import { getEffectiveDiscordBotToken } from '../channels/discord.js';
|
|
6
|
+
import { registerSpecTools } from './tool/spec-loader.js';
|
|
8
7
|
import { appendMemory, listMemories, getSharedScopeKeyForSession, } from './memory.js';
|
|
9
8
|
import { addWatchPaths } from '../watcher/index.js';
|
|
10
9
|
import { createPendingAction } from './pending-actions.js';
|
|
11
|
-
import { getAllRequiredBins } from './skills.js';
|
|
12
10
|
import { getSkillConfigEnv, getSkillToolPolicy } from './skills-config.js';
|
|
13
11
|
import { redactSecretKeysInSummary } from '../redact.js';
|
|
14
12
|
import { runAgentTurn } from './loop.js';
|
|
@@ -134,11 +132,11 @@ export function executeTool(name, args, opts) {
|
|
|
134
132
|
export function registerBuiltInTools(enqueueTask) {
|
|
135
133
|
registerTool({
|
|
136
134
|
name: 'run_task',
|
|
137
|
-
description: 'Enqueue a background task by type and optional payload. Use for
|
|
135
|
+
description: 'Enqueue a background task by type and optional payload. Use ONLY for system/scheduled task types (e.g. heartbeat, file_event, agent_job). Do NOT use for user-requested immediate results: for read/summarize email, list calendar, create invoice, or similar, use the integration tools (list_integrations_connections, get_connection_token, run_command, stripe_*) and return the result in this turn.',
|
|
138
136
|
parameters: {
|
|
139
137
|
type: 'object',
|
|
140
138
|
properties: {
|
|
141
|
-
type: { type: 'string', description: 'Task type (e.g. heartbeat, file_event)' },
|
|
139
|
+
type: { type: 'string', description: 'Task type (e.g. heartbeat, file_event, agent_job)' },
|
|
142
140
|
payload: { type: 'object', description: 'Optional JSON payload' },
|
|
143
141
|
},
|
|
144
142
|
required: ['type'],
|
|
@@ -153,167 +151,157 @@ export function registerBuiltInTools(enqueueTask) {
|
|
|
153
151
|
return { taskId, type, status: 'enqueued' };
|
|
154
152
|
},
|
|
155
153
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
type: 'object',
|
|
169
|
-
properties: {
|
|
170
|
-
binary: { type: 'string', description: 'Executable name (e.g. memo, git). Must be in ALLOWED_BINARIES.' },
|
|
171
|
-
args: {
|
|
172
|
-
type: 'array',
|
|
173
|
-
items: { type: 'string' },
|
|
174
|
-
description: 'Arguments (e.g. ["notes", "-a", "buy saiko"] for memo).',
|
|
175
|
-
},
|
|
154
|
+
registerTool({
|
|
155
|
+
name: 'run_command',
|
|
156
|
+
description: 'Run a single command (binary + args). Use for skills that document CLI usage (e.g. memo for Apple Notes, osascript): read the skill doc, then run the commands it describes.',
|
|
157
|
+
profile: 'coding',
|
|
158
|
+
parameters: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
binary: { type: 'string', description: 'Executable name (e.g. osascript, memo, git, curl).' },
|
|
162
|
+
args: {
|
|
163
|
+
type: 'array',
|
|
164
|
+
items: { type: 'string' },
|
|
165
|
+
description: 'Arguments (e.g. ["notes", "-a", "buy saiko"] for memo).',
|
|
176
166
|
},
|
|
177
|
-
required: ['binary', 'args'],
|
|
178
167
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return !u.hostname || u.hostname.includes('$');
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
})();
|
|
228
|
-
if (isBroken || urlArg !== resolved) {
|
|
229
|
-
console.log('agent', 'info', 'run_command: resolved portal URL for curl', {
|
|
230
|
-
host: new URL(resolved).hostname,
|
|
231
|
-
path: pathSuffix,
|
|
232
|
-
});
|
|
233
|
-
argsList = [...argsList.slice(0, urlIdx), resolved, ...argsList.slice(urlIdx + 1)];
|
|
234
|
-
if (argsList[urlIdx + 1] === path) {
|
|
235
|
-
argsList = [...argsList.slice(0, urlIdx + 1), ...argsList.slice(urlIdx + 2)];
|
|
168
|
+
required: ['binary', 'args'],
|
|
169
|
+
},
|
|
170
|
+
execute: (args) => {
|
|
171
|
+
const binary = String(args.binary || '').trim();
|
|
172
|
+
if (!binary)
|
|
173
|
+
return { error: 'binary is required' };
|
|
174
|
+
const rawArgs = Array.isArray(args.args) ? args.args : [];
|
|
175
|
+
let argsList = rawArgs.map((a) => String(a).replace(/\0/g, '').trim()).filter((_, i) => i < 50);
|
|
176
|
+
// Expand $PORTAL_GATEWAY_URL and $PORTAL_API_KEY in args (no shell in spawnSync, so LLM's $VAR stays literal and curl gets "Couldn't resolve host")
|
|
177
|
+
const portalGatewayBase = getPortalGatewayBase() || process.env.PORTAL_GATEWAY_URL || '';
|
|
178
|
+
const portalApiKey = getEffectivePortalApiKey() || process.env.PORTAL_API_KEY || '';
|
|
179
|
+
if (portalGatewayBase || portalApiKey) {
|
|
180
|
+
argsList = argsList.map((a) => {
|
|
181
|
+
let s = a;
|
|
182
|
+
if (portalGatewayBase && s.includes('$PORTAL_GATEWAY_URL'))
|
|
183
|
+
s = s.replace(/\$PORTAL_GATEWAY_URL/g, portalGatewayBase);
|
|
184
|
+
if (portalApiKey && s.includes('$PORTAL_API_KEY'))
|
|
185
|
+
s = s.replace(/\$PORTAL_API_KEY/g, portalApiKey);
|
|
186
|
+
return s;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Resolve portal gateway URLs for curl: .../connections/conn_xxx/use and .../connections/conn_xxx/bsky-request
|
|
190
|
+
if (binary === 'curl' && portalGatewayBase && argsList.length > 0) {
|
|
191
|
+
const argsStr = argsList.join(' ');
|
|
192
|
+
const useMatch = argsStr.match(/\/connections\/(conn_[a-zA-Z0-9_]+)\/use/);
|
|
193
|
+
const bskyMatch = argsStr.match(/\/connections\/(conn_[a-zA-Z0-9_]+)\/bsky-request/);
|
|
194
|
+
const pathMatch = useMatch || bskyMatch;
|
|
195
|
+
const pathSuffix = useMatch ? '/use' : bskyMatch ? '/bsky-request' : '';
|
|
196
|
+
if (pathMatch) {
|
|
197
|
+
const path = `/connections/${pathMatch[1]}${pathSuffix}`;
|
|
198
|
+
const resolved = portalGatewayBase.replace(/\/$/, '') + path;
|
|
199
|
+
const postIdx = argsList.indexOf('POST');
|
|
200
|
+
const urlIdx = postIdx >= 0
|
|
201
|
+
? postIdx + 1
|
|
202
|
+
: argsList.findIndex((a) => a.includes('/connections/') && (a.includes('/use') || a.includes('/bsky-request')));
|
|
203
|
+
if (urlIdx >= 0 && urlIdx < argsList.length) {
|
|
204
|
+
const urlArg = argsList[urlIdx];
|
|
205
|
+
const isBroken = urlArg.includes('$') ||
|
|
206
|
+
!urlArg.startsWith('http') ||
|
|
207
|
+
(() => {
|
|
208
|
+
try {
|
|
209
|
+
const u = new URL(urlArg);
|
|
210
|
+
return !u.hostname || u.hostname.includes('$');
|
|
236
211
|
}
|
|
212
|
+
catch {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
if (isBroken || urlArg !== resolved) {
|
|
217
|
+
console.log('agent', 'info', 'run_command: resolved portal URL for curl', {
|
|
218
|
+
host: new URL(resolved).hostname,
|
|
219
|
+
path: pathSuffix,
|
|
220
|
+
});
|
|
221
|
+
argsList = [...argsList.slice(0, urlIdx), resolved, ...argsList.slice(urlIdx + 1)];
|
|
222
|
+
if (argsList[urlIdx + 1] === path) {
|
|
223
|
+
argsList = [...argsList.slice(0, urlIdx + 1), ...argsList.slice(urlIdx + 2)];
|
|
237
224
|
}
|
|
238
225
|
}
|
|
239
226
|
}
|
|
240
227
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
catch {
|
|
253
|
-
// skip malformed URL
|
|
254
|
-
}
|
|
228
|
+
}
|
|
229
|
+
if (binary === 'curl') {
|
|
230
|
+
const allowedHosts = (process.env.ALLOWED_CURL_HOSTS || '').split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
231
|
+
if (allowedHosts.length > 0) {
|
|
232
|
+
const urls = argsList.join(' ').match(/https?:\/\/[^/\s"'<>]+/g) || [];
|
|
233
|
+
for (const u of urls) {
|
|
234
|
+
try {
|
|
235
|
+
const host = new URL(u).hostname.toLowerCase();
|
|
236
|
+
const allowed = allowedHosts.some((h) => host === h || host.endsWith('.' + h));
|
|
237
|
+
if (!allowed)
|
|
238
|
+
return { error: `curl URL host not allowed: ${host} (ALLOWED_CURL_HOSTS)` };
|
|
255
239
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
const skillEnv = getSkillConfigEnv();
|
|
260
|
-
const env = { ...process.env, ...skillEnv };
|
|
261
|
-
if (process.env.DEBUG || process.env.SULALA_LOG_SKILL_ENV) {
|
|
262
|
-
const keys = Object.keys(skillEnv).sort();
|
|
263
|
-
const summary = keys.map((k) => {
|
|
264
|
-
const v = skillEnv[k];
|
|
265
|
-
const len = typeof v === 'string' ? v.length : 0;
|
|
266
|
-
return `${k} (${len > 0 ? `${len} chars` : 'empty'})`;
|
|
267
|
-
});
|
|
268
|
-
console.log('agent', 'info', `run_command skill env: ${redactSecretKeysInSummary(summary).join(', ')}`, { binary, keyCount: keys.length });
|
|
269
|
-
}
|
|
270
|
-
const result = spawnSync(binary, argsList, { encoding: 'utf8', timeout: 30000, env });
|
|
271
|
-
if (result.error)
|
|
272
|
-
return { error: result.error.message };
|
|
273
|
-
// Log token request result so we can confirm gateway returned accessToken (do not log the token)
|
|
274
|
-
if (binary === 'curl' && argsList.length > 0) {
|
|
275
|
-
const argsStr = argsList.join(' ');
|
|
276
|
-
const tokenUseMatch = argsStr.match(/\/connections\/(conn_[a-zA-Z0-9_]+)\/use/);
|
|
277
|
-
if (tokenUseMatch) {
|
|
278
|
-
const stdout = result.stdout ?? '';
|
|
279
|
-
const gotToken = stdout.includes('accessToken') && !stdout.trimStart().startsWith('{"error"');
|
|
280
|
-
console.log('agent', 'info', 'run_command: token request (POST .../connections/<id>/use) result', {
|
|
281
|
-
connection_id: tokenUseMatch[1],
|
|
282
|
-
gotToken,
|
|
283
|
-
exitCode: result.status ?? null,
|
|
284
|
-
});
|
|
240
|
+
catch {
|
|
241
|
+
// skip malformed URL
|
|
285
242
|
}
|
|
286
243
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const skillEnv = getSkillConfigEnv();
|
|
248
|
+
const env = { ...process.env, ...skillEnv };
|
|
249
|
+
if (process.env.DEBUG || process.env.SULALA_LOG_SKILL_ENV) {
|
|
250
|
+
const keys = Object.keys(skillEnv).sort();
|
|
251
|
+
const summary = keys.map((k) => {
|
|
252
|
+
const v = skillEnv[k];
|
|
253
|
+
const len = typeof v === 'string' ? v.length : 0;
|
|
254
|
+
return `${k} (${len > 0 ? `${len} chars` : 'empty'})`;
|
|
255
|
+
});
|
|
256
|
+
console.log('agent', 'info', `run_command skill env: ${redactSecretKeysInSummary(summary).join(', ')}`, { binary, keyCount: keys.length });
|
|
257
|
+
}
|
|
258
|
+
const result = spawnSync(binary, argsList, { encoding: 'utf8', timeout: 30000, env });
|
|
259
|
+
if (result.error)
|
|
260
|
+
return { error: result.error.message };
|
|
261
|
+
// Log token request result so we can confirm gateway returned accessToken (do not log the token)
|
|
262
|
+
if (binary === 'curl' && argsList.length > 0) {
|
|
263
|
+
const argsStr = argsList.join(' ');
|
|
264
|
+
const tokenUseMatch = argsStr.match(/\/connections\/(conn_[a-zA-Z0-9_]+)\/use/);
|
|
265
|
+
if (tokenUseMatch) {
|
|
295
266
|
const stdout = result.stdout ?? '';
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
267
|
+
const gotToken = stdout.includes('accessToken') && !stdout.trimStart().startsWith('{"error"');
|
|
268
|
+
console.log('agent', 'info', 'run_command: token request (POST .../connections/<id>/use) result', {
|
|
269
|
+
connection_id: tokenUseMatch[1],
|
|
270
|
+
gotToken,
|
|
271
|
+
exitCode: result.status ?? null,
|
|
272
|
+
});
|
|
302
273
|
}
|
|
303
|
-
return out;
|
|
304
274
|
}
|
|
305
|
-
|
|
306
|
-
|
|
275
|
+
const out = {
|
|
276
|
+
status: result.status ?? null,
|
|
277
|
+
stdout: result.stdout?.trim() ?? '',
|
|
278
|
+
stderr: result.stderr?.trim() ?? '',
|
|
279
|
+
};
|
|
280
|
+
// When agent calls Gmail/Calendar/Google API without token, 401 is returned. Add a hint so the model retries with token-first flow.
|
|
281
|
+
if (binary === 'curl' && argsList.length > 0) {
|
|
282
|
+
const argsStr = argsList.join(' ');
|
|
283
|
+
const stdout = result.stdout ?? '';
|
|
284
|
+
const isGoogleApi = argsStr.includes('gmail.googleapis.com') || argsStr.includes('www.googleapis.com') || argsStr.includes('sheets.googleapis.com');
|
|
285
|
+
const is401 = stdout.includes('401') && (stdout.includes('invalid authentication') || stdout.includes('invalid credentials'));
|
|
286
|
+
if (isGoogleApi && is401) {
|
|
287
|
+
out._hint =
|
|
288
|
+
'Gmail/Calendar/Google API returned 401. Get an OAuth token first: call get_connection_token(connection_id) with the connection_id from list_integrations_connections, then call the Gmail/API URL again with -H "Authorization: Bearer <accessToken>" using the token from that result.';
|
|
289
|
+
}
|
|
307
290
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
return { error: e.message };
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
});
|
|
311
298
|
if (config.agentWorkspaceRoot || config.workspaceDir) {
|
|
312
299
|
const workspaceRoot = config.agentWorkspaceRoot ? resolve(process.cwd(), config.agentWorkspaceRoot) : null;
|
|
313
300
|
const workspaceDirResolved = config.workspaceDir ? resolve(config.workspaceDir) : null;
|
|
314
301
|
const workspaceSkillsDir = config.skillsWorkspaceDir ? resolve(config.skillsWorkspaceDir) : null;
|
|
302
|
+
const workspaceSkillsMyDir = config.skillsWorkspaceMyDir ? resolve(config.skillsWorkspaceMyDir) : null;
|
|
315
303
|
const sulalaHomeDir = config.workspaceDir ? resolve(config.workspaceDir, '..') : null;
|
|
316
|
-
const allowedReadRoots = [workspaceRoot, workspaceDirResolved, workspaceSkillsDir, sulalaHomeDir].filter(Boolean);
|
|
304
|
+
const allowedReadRoots = [workspaceRoot, workspaceDirResolved, workspaceSkillsDir, workspaceSkillsMyDir, sulalaHomeDir].filter(Boolean);
|
|
317
305
|
const defaultRoot = workspaceRoot ?? workspaceDirResolved ?? process.cwd();
|
|
318
306
|
registerTool({
|
|
319
307
|
name: 'read_file',
|
|
@@ -347,10 +335,10 @@ export function registerBuiltInTools(enqueueTask) {
|
|
|
347
335
|
}
|
|
348
336
|
},
|
|
349
337
|
});
|
|
350
|
-
const allowedWriteRoots = [workspaceRoot, workspaceDirResolved, workspaceSkillsDir].filter(Boolean);
|
|
338
|
+
const allowedWriteRoots = [workspaceRoot, workspaceDirResolved, workspaceSkillsDir, workspaceSkillsMyDir].filter(Boolean);
|
|
351
339
|
registerTool({
|
|
352
340
|
name: 'write_file',
|
|
353
|
-
description: 'Write content to a file. Use for scripts in workspace/scripts/, credentials in workspace/.env, or skills in workspace/skills
|
|
341
|
+
description: 'Write content to a file. Use for scripts in workspace/scripts/, credentials in workspace/.env, or skills you create in workspace/skills/my/<slug>/README.md. Path can be relative to workspace root or absolute.',
|
|
354
342
|
profile: 'coding',
|
|
355
343
|
parameters: {
|
|
356
344
|
type: 'object',
|
|
@@ -564,10 +552,10 @@ export function registerBuiltInTools(enqueueTask) {
|
|
|
564
552
|
const filtered = list.filter((c) => (c.provider || '').toLowerCase() === p);
|
|
565
553
|
return filtered.map((c) => ({ connection_id: c.connection_id ?? c.id ?? '', provider: c.provider }));
|
|
566
554
|
}
|
|
567
|
-
/** List OAuth connections
|
|
555
|
+
/** List OAuth connections only (calendar, gmail, drive, github, slack, etc.). Not for Stripe or Discord—use stripe_list_customers / discord_* instead. */
|
|
568
556
|
registerTool({
|
|
569
557
|
name: 'list_integrations_connections',
|
|
570
|
-
description: '
|
|
558
|
+
description: 'OAuth integrations only (calendar, gmail, drive, docs, sheets, slides, github, slack, notion, linear, zoom). Returns connection_id and provider. Do NOT use for Stripe or Discord—for Stripe customers use stripe_list_customers; for Discord use discord_list_guilds / discord_send_message. Requires PORTAL_GATEWAY_URL + PORTAL_API_KEY or INTEGRATIONS_URL.',
|
|
571
559
|
profile: 'full',
|
|
572
560
|
parameters: {
|
|
573
561
|
type: 'object',
|
|
@@ -692,221 +680,10 @@ export function registerBuiltInTools(enqueueTask) {
|
|
|
692
680
|
}
|
|
693
681
|
},
|
|
694
682
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
name
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
parameters: {
|
|
701
|
-
type: 'object',
|
|
702
|
-
properties: {
|
|
703
|
-
connection_id: { type: 'string', description: 'Bluesky connection ID from list_integrations_connections (e.g. conn_bluesky_...).' },
|
|
704
|
-
text: { type: 'string', description: 'Post text (max 300 characters).' },
|
|
705
|
-
},
|
|
706
|
-
required: ['connection_id', 'text'],
|
|
707
|
-
},
|
|
708
|
-
execute: async (args) => {
|
|
709
|
-
const connectionId = typeof args.connection_id === 'string' ? args.connection_id.trim() : '';
|
|
710
|
-
const text = typeof args.text === 'string' ? args.text.trim() : '';
|
|
711
|
-
if (!connectionId)
|
|
712
|
-
return { error: 'connection_id is required' };
|
|
713
|
-
if (!text)
|
|
714
|
-
return { error: 'text is required' };
|
|
715
|
-
if (text.length > 300)
|
|
716
|
-
return { error: 'Bluesky posts are limited to 300 characters' };
|
|
717
|
-
const portalGatewayBase = getPortalGatewayBase();
|
|
718
|
-
const portalKey = getEffectivePortalApiKey();
|
|
719
|
-
if (!portalGatewayBase || !portalKey)
|
|
720
|
-
return { error: 'Set PORTAL_GATEWAY_URL and PORTAL_API_KEY (from Portal → API Keys)' };
|
|
721
|
-
const base = portalGatewayBase.replace(/\/$/, '');
|
|
722
|
-
try {
|
|
723
|
-
const useRes = await fetch(`${base}/connections/${encodeURIComponent(connectionId)}/use`, {
|
|
724
|
-
method: 'POST',
|
|
725
|
-
headers: { Authorization: `Bearer ${portalKey}` },
|
|
726
|
-
});
|
|
727
|
-
if (!useRes.ok) {
|
|
728
|
-
const t = await useRes.text();
|
|
729
|
-
return { error: `Portal gateway use: ${useRes.status}`, detail: t.slice(0, 200) };
|
|
730
|
-
}
|
|
731
|
-
const useData = (await useRes.json());
|
|
732
|
-
if (useData.error)
|
|
733
|
-
return { error: useData.error };
|
|
734
|
-
if (!useData.useProxy || !useData.blueskyDid) {
|
|
735
|
-
return { error: 'Not a Bluesky connection or missing blueskyDid; reconnect Bluesky in the Portal.' };
|
|
736
|
-
}
|
|
737
|
-
const createdAt = new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');
|
|
738
|
-
const body = {
|
|
739
|
-
path: '/xrpc/com.atproto.repo.createRecord',
|
|
740
|
-
method: 'POST',
|
|
741
|
-
body: {
|
|
742
|
-
repo: useData.blueskyDid,
|
|
743
|
-
collection: 'app.bsky.feed.post',
|
|
744
|
-
record: { $type: 'app.bsky.feed.post', text, createdAt },
|
|
745
|
-
},
|
|
746
|
-
};
|
|
747
|
-
const bskyRes = await fetch(`${base}/connections/${encodeURIComponent(connectionId)}/bsky-request`, {
|
|
748
|
-
method: 'POST',
|
|
749
|
-
headers: { Authorization: `Bearer ${portalKey}`, 'Content-Type': 'application/json' },
|
|
750
|
-
body: JSON.stringify(body),
|
|
751
|
-
});
|
|
752
|
-
const resText = await bskyRes.text();
|
|
753
|
-
if (!bskyRes.ok)
|
|
754
|
-
return { error: `Bluesky request: ${bskyRes.status}`, detail: resText.slice(0, 300) };
|
|
755
|
-
let parsed;
|
|
756
|
-
try {
|
|
757
|
-
parsed = JSON.parse(resText);
|
|
758
|
-
}
|
|
759
|
-
catch {
|
|
760
|
-
return { error: 'Invalid JSON from Bluesky', detail: resText.slice(0, 200) };
|
|
761
|
-
}
|
|
762
|
-
if (parsed.error)
|
|
763
|
-
return { error: parsed.error };
|
|
764
|
-
if (!parsed.uri)
|
|
765
|
-
return { error: 'Post may have failed; response had no uri', detail: resText.slice(0, 200) };
|
|
766
|
-
return { ok: true, uri: parsed.uri, message: 'Posted to Bluesky successfully.' };
|
|
767
|
-
}
|
|
768
|
-
catch (e) {
|
|
769
|
-
return { error: e.message };
|
|
770
|
-
}
|
|
771
|
-
},
|
|
772
|
-
});
|
|
773
|
-
/** Stripe: list customers using key from Settings → Channels (or STRIPE_SECRET_KEY). Do not use list_integrations_connections for Stripe. */
|
|
774
|
-
registerTool({
|
|
775
|
-
name: 'stripe_list_customers',
|
|
776
|
-
description: 'List Stripe customers. Uses the Stripe secret key from Settings → Channels (Stripe) or STRIPE_SECRET_KEY. Do not use list_integrations_connections for Stripe. Returns customers with id, email, name; or an error if Stripe is not configured.',
|
|
777
|
-
profile: 'full',
|
|
778
|
-
parameters: {
|
|
779
|
-
type: 'object',
|
|
780
|
-
properties: {
|
|
781
|
-
limit: { type: 'number', description: 'Max customers to return (default 10, max 100)' },
|
|
782
|
-
},
|
|
783
|
-
required: [],
|
|
784
|
-
},
|
|
785
|
-
execute: async (args) => {
|
|
786
|
-
const key = getEffectiveStripeSecretKey();
|
|
787
|
-
if (!key?.trim())
|
|
788
|
-
return { error: 'Stripe is not configured. Add a secret key in Settings → Channels (Stripe) or set STRIPE_SECRET_KEY.' };
|
|
789
|
-
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(Math.floor(args.limit), 100) : 10;
|
|
790
|
-
try {
|
|
791
|
-
const url = `https://api.stripe.com/v1/customers?limit=${limit}`;
|
|
792
|
-
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
|
|
793
|
-
if (res.status === 401)
|
|
794
|
-
return { error: 'Invalid Stripe secret key (unauthorized)' };
|
|
795
|
-
if (!res.ok) {
|
|
796
|
-
const text = await res.text();
|
|
797
|
-
return { error: `Stripe API: ${res.status}`, detail: text.slice(0, 200) };
|
|
798
|
-
}
|
|
799
|
-
const json = (await res.json());
|
|
800
|
-
const data = json.data ?? [];
|
|
801
|
-
const customers = data.map((c) => ({ id: c.id, email: c.email, name: c.name }));
|
|
802
|
-
return { customers, count: customers.length };
|
|
803
|
-
}
|
|
804
|
-
catch (e) {
|
|
805
|
-
return { error: e.message };
|
|
806
|
-
}
|
|
807
|
-
},
|
|
808
|
-
});
|
|
809
|
-
/** Discord: list guilds (servers) using bot token from Settings → Channels. Do not use list_integrations_connections for Discord. */
|
|
810
|
-
registerTool({
|
|
811
|
-
name: 'discord_list_guilds',
|
|
812
|
-
description: 'List Discord servers (guilds) the bot is in. Uses the bot token from Settings → Channels (Discord) or DISCORD_BOT_TOKEN. Do not use list_integrations_connections for Discord. Returns guilds with id and name; or an error if Discord is not configured.',
|
|
813
|
-
profile: 'full',
|
|
814
|
-
parameters: { type: 'object', properties: {}, required: [] },
|
|
815
|
-
execute: async () => {
|
|
816
|
-
const token = getEffectiveDiscordBotToken();
|
|
817
|
-
if (!token?.trim())
|
|
818
|
-
return { error: 'Discord is not configured. Add a bot token in Settings → Channels (Discord) or set DISCORD_BOT_TOKEN.' };
|
|
819
|
-
try {
|
|
820
|
-
const res = await fetch('https://discord.com/api/v10/users/@me/guilds', {
|
|
821
|
-
headers: { Authorization: `Bot ${token}` },
|
|
822
|
-
});
|
|
823
|
-
if (!res.ok) {
|
|
824
|
-
const text = await res.text();
|
|
825
|
-
return { error: `Discord API: ${res.status}`, detail: text.slice(0, 200) };
|
|
826
|
-
}
|
|
827
|
-
const list = (await res.json());
|
|
828
|
-
return { guilds: list.map((g) => ({ id: g.id, name: g.name })), count: list.length };
|
|
829
|
-
}
|
|
830
|
-
catch (e) {
|
|
831
|
-
return { error: e.message };
|
|
832
|
-
}
|
|
833
|
-
},
|
|
834
|
-
});
|
|
835
|
-
/** Discord: list channels in a guild. */
|
|
836
|
-
registerTool({
|
|
837
|
-
name: 'discord_list_channels',
|
|
838
|
-
description: 'List channels in a Discord server (guild). Uses the bot token from Settings → Channels. Call discord_list_guilds first to get guild_id. Returns channels with id, name, type (0=text, 2=voice, 4=category).',
|
|
839
|
-
profile: 'full',
|
|
840
|
-
parameters: {
|
|
841
|
-
type: 'object',
|
|
842
|
-
properties: {
|
|
843
|
-
guild_id: { type: 'string', description: 'Discord guild (server) ID from discord_list_guilds' },
|
|
844
|
-
},
|
|
845
|
-
required: ['guild_id'],
|
|
846
|
-
},
|
|
847
|
-
execute: async (args) => {
|
|
848
|
-
const token = getEffectiveDiscordBotToken();
|
|
849
|
-
if (!token?.trim())
|
|
850
|
-
return { error: 'Discord is not configured. Add a bot token in Settings → Channels (Discord) or set DISCORD_BOT_TOKEN.' };
|
|
851
|
-
const guildId = typeof args.guild_id === 'string' ? args.guild_id.trim() : '';
|
|
852
|
-
if (!guildId)
|
|
853
|
-
return { error: 'guild_id is required' };
|
|
854
|
-
try {
|
|
855
|
-
const res = await fetch(`https://discord.com/api/v10/guilds/${encodeURIComponent(guildId)}/channels`, {
|
|
856
|
-
headers: { Authorization: `Bot ${token}` },
|
|
857
|
-
});
|
|
858
|
-
if (!res.ok) {
|
|
859
|
-
const text = await res.text();
|
|
860
|
-
return { error: `Discord API: ${res.status}`, detail: text.slice(0, 200) };
|
|
861
|
-
}
|
|
862
|
-
const list = (await res.json());
|
|
863
|
-
return { channels: list.map((c) => ({ id: c.id, name: c.name, type: c.type })), count: list.length };
|
|
864
|
-
}
|
|
865
|
-
catch (e) {
|
|
866
|
-
return { error: e.message };
|
|
867
|
-
}
|
|
868
|
-
},
|
|
869
|
-
});
|
|
870
|
-
/** Discord: send a message to a channel. */
|
|
871
|
-
registerTool({
|
|
872
|
-
name: 'discord_send_message',
|
|
873
|
-
description: 'Send a message to a Discord channel. Uses the bot token from Settings → Channels. Call discord_list_guilds then discord_list_channels to get channel_id. Max content length 2000 characters.',
|
|
874
|
-
profile: 'full',
|
|
875
|
-
parameters: {
|
|
876
|
-
type: 'object',
|
|
877
|
-
properties: {
|
|
878
|
-
channel_id: { type: 'string', description: 'Discord channel ID from discord_list_channels' },
|
|
879
|
-
content: { type: 'string', description: 'Message text (max 2000 chars)' },
|
|
880
|
-
},
|
|
881
|
-
required: ['channel_id', 'content'],
|
|
882
|
-
},
|
|
883
|
-
execute: async (args) => {
|
|
884
|
-
const token = getEffectiveDiscordBotToken();
|
|
885
|
-
if (!token?.trim())
|
|
886
|
-
return { error: 'Discord is not configured. Add a bot token in Settings → Channels (Discord) or set DISCORD_BOT_TOKEN.' };
|
|
887
|
-
const channelId = typeof args.channel_id === 'string' ? args.channel_id.trim() : '';
|
|
888
|
-
let content = typeof args.content === 'string' ? args.content : '';
|
|
889
|
-
if (!channelId)
|
|
890
|
-
return { error: 'channel_id is required' };
|
|
891
|
-
if (content.length > 2000)
|
|
892
|
-
content = content.slice(0, 2000);
|
|
893
|
-
try {
|
|
894
|
-
const res = await fetch(`https://discord.com/api/v10/channels/${encodeURIComponent(channelId)}/messages`, {
|
|
895
|
-
method: 'POST',
|
|
896
|
-
headers: { Authorization: `Bot ${token}`, 'Content-Type': 'application/json' },
|
|
897
|
-
body: JSON.stringify({ content }),
|
|
898
|
-
});
|
|
899
|
-
if (!res.ok) {
|
|
900
|
-
const text = await res.text();
|
|
901
|
-
return { error: `Discord API: ${res.status}`, detail: text.slice(0, 200) };
|
|
902
|
-
}
|
|
903
|
-
const data = (await res.json());
|
|
904
|
-
return { ok: true, message_id: data.id };
|
|
905
|
-
}
|
|
906
|
-
catch (e) {
|
|
907
|
-
return { error: e.message };
|
|
908
|
-
}
|
|
909
|
-
},
|
|
910
|
-
});
|
|
683
|
+
// Register YAML spec tools; first-wins so ~/.sulala/workspace/skills (e.g. stripe) overrides ./context
|
|
684
|
+
registerSpecTools((tool) => {
|
|
685
|
+
if (!registry.has(tool.name))
|
|
686
|
+
registerTool(tool);
|
|
687
|
+
}, config);
|
|
911
688
|
}
|
|
912
689
|
//# sourceMappingURL=tools.js.map
|