@almanak/mcp-server 0.2.0 → 0.2.2
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/client.d.ts +66 -11
- package/dist/client.js +33 -13
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/testing/__tests__/platform-client.test.js +66 -0
- package/dist/testing/__tests__/platform-client.test.js.map +1 -1
- package/dist/testing/__tests__/supported-protocols-tool.test.d.ts +1 -0
- package/dist/testing/__tests__/supported-protocols-tool.test.js +35 -0
- package/dist/testing/__tests__/supported-protocols-tool.test.js.map +1 -0
- package/dist/testing/__tests__/tools.test.js +0 -71
- package/dist/testing/__tests__/tools.test.js.map +1 -1
- package/dist/testing/__tests__/workspace-export-tools.test.d.ts +1 -0
- package/dist/testing/__tests__/workspace-export-tools.test.js +439 -0
- package/dist/testing/__tests__/workspace-export-tools.test.js.map +1 -0
- package/dist/tools.d.ts +33 -0
- package/dist/tools.js +357 -81
- package/dist/tools.js.map +1 -1
- package/dist/workspace-git.d.ts +91 -0
- package/dist/workspace-git.js +374 -0
- package/dist/workspace-git.js.map +1 -0
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -49,6 +49,9 @@ exports.PlatformToolHandler = void 0;
|
|
|
49
49
|
const node_fs_1 = require("node:fs");
|
|
50
50
|
const path = __importStar(require("node:path"));
|
|
51
51
|
const runner_1 = require("./testing/runner");
|
|
52
|
+
const workspace_git_1 = require("./workspace-git");
|
|
53
|
+
const GIT_PROVIDERS = new Set(['almanak', 'github']);
|
|
54
|
+
const EXPORT_COMMIT_MESSAGE = 'feat: initial strategy export from Almanak';
|
|
52
55
|
const ROOT_DIR_ALLOWED_RE = /^[a-zA-Z0-9._/-]+$/;
|
|
53
56
|
const WORKSPACE_ABSOLUTE_PATH_RE = /^\/(?:tmp\/)?workspaces\/[^/]+\/(.+)$/;
|
|
54
57
|
const WORKSPACE_ROOT_PATH_RE = /^\/(?:tmp\/)?workspaces\/[^/]+\/?$/;
|
|
@@ -194,7 +197,9 @@ class PlatformToolHandler {
|
|
|
194
197
|
this.options = {
|
|
195
198
|
modes: ['all'],
|
|
196
199
|
disabledTools: [],
|
|
200
|
+
sdkGeneration: undefined,
|
|
197
201
|
testController: null,
|
|
202
|
+
workspaceRoot: undefined,
|
|
198
203
|
...options,
|
|
199
204
|
};
|
|
200
205
|
this.enabledModes = new Set(this.options.modes);
|
|
@@ -254,7 +259,7 @@ class PlatformToolHandler {
|
|
|
254
259
|
* is unavailable. Best effort by design: a failed check must never block the export flow.
|
|
255
260
|
*/
|
|
256
261
|
async getLinkedSessionRepo() {
|
|
257
|
-
var _a;
|
|
262
|
+
var _a, _b;
|
|
258
263
|
try {
|
|
259
264
|
const response = await this.client.getSessionRepoLink();
|
|
260
265
|
const data = ((_a = response === null || response === void 0 ? void 0 : response.data) !== null && _a !== void 0 ? _a : response);
|
|
@@ -264,19 +269,321 @@ class PlatformToolHandler {
|
|
|
264
269
|
const repoName = typeof data.repoName === 'string' ? data.repoName : '';
|
|
265
270
|
if (!repoOwner || !repoName)
|
|
266
271
|
return null;
|
|
272
|
+
const str = (key) => (typeof data[key] === 'string' ? data[key] : null);
|
|
267
273
|
return {
|
|
268
274
|
repoOwner,
|
|
269
275
|
repoName,
|
|
270
276
|
// The backend URL is provider-aware (Almanak Git or GitHub) — never assume a github.com host here.
|
|
271
|
-
repoUrl:
|
|
272
|
-
provider:
|
|
273
|
-
strategyLinkId:
|
|
277
|
+
repoUrl: (_b = str('repoUrl')) !== null && _b !== void 0 ? _b : '',
|
|
278
|
+
provider: str('provider'),
|
|
279
|
+
strategyLinkId: str('strategyLinkId'),
|
|
280
|
+
repoId: str('repoId'),
|
|
281
|
+
cloneUrl: str('cloneUrl'),
|
|
282
|
+
gitHost: str('gitHost'),
|
|
283
|
+
defaultBranch: str('defaultBranch'),
|
|
274
284
|
};
|
|
275
285
|
}
|
|
276
286
|
catch {
|
|
277
287
|
return null;
|
|
278
288
|
}
|
|
279
289
|
}
|
|
290
|
+
/** Workspace root for git-backed tools (see ToolHandlerOptions.workspaceRoot). */
|
|
291
|
+
get workspaceRoot() {
|
|
292
|
+
var _a;
|
|
293
|
+
return (_a = this.options.workspaceRoot) !== null && _a !== void 0 ? _a : process.cwd();
|
|
294
|
+
}
|
|
295
|
+
gitOpts(sessionId, timeoutMs) {
|
|
296
|
+
return { cwd: this.workspaceRoot, sessionId, ...(timeoutMs !== undefined ? { timeoutMs } : {}) };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Map a failed `POST /agent-sessions/{id}/repository` response to the tool's status vocabulary.
|
|
300
|
+
* Returns null when the response is a success.
|
|
301
|
+
*/
|
|
302
|
+
mapLinkFailure(res) {
|
|
303
|
+
if (res && res.valid !== false && !(typeof res.statusCode === 'number' && res.statusCode >= 400) && res.data)
|
|
304
|
+
return null;
|
|
305
|
+
const message = typeof (res === null || res === void 0 ? void 0 : res.error) === 'string' ? res.error : typeof (res === null || res === void 0 ? void 0 : res.message) === 'string' ? res.message : 'Repository link failed';
|
|
306
|
+
const code = typeof (res === null || res === void 0 ? void 0 : res.errorCode) === 'string' ? res.errorCode : null;
|
|
307
|
+
switch (code) {
|
|
308
|
+
case 'name_conflict':
|
|
309
|
+
return { status: 'name_conflict', message: `${message} Choose a different repoName and call the tool again.` };
|
|
310
|
+
case 'github_connection_required':
|
|
311
|
+
return { status: 'github_connection_required', message };
|
|
312
|
+
case 'provider_disabled':
|
|
313
|
+
return { status: 'provider_disabled', message };
|
|
314
|
+
case 'repo_config_in_progress':
|
|
315
|
+
return { status: 'in_progress', message: `${message} Another link operation is still running for this session; wait and retry.` };
|
|
316
|
+
default:
|
|
317
|
+
return { status: 'failed', statusCode: typeof (res === null || res === void 0 ? void 0 : res.statusCode) === 'number' ? res.statusCode : 502, error: message, ...(code ? { errorCode: code } : {}) };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* strategies_export_workspace: create + link a new repository, then init/commit/push the
|
|
322
|
+
* workspace from inside this call. Every step is synchronous so the agent can continue in
|
|
323
|
+
* the same turn on `completed`.
|
|
324
|
+
*/
|
|
325
|
+
async exportWorkspace(args) {
|
|
326
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
327
|
+
const repoName = typeof args.repoName === 'string' ? args.repoName.trim() : '';
|
|
328
|
+
if (!repoName)
|
|
329
|
+
return { statusCode: 400, status: 'failed', error: 'repoName is required' };
|
|
330
|
+
const description = typeof args.description === 'string' && args.description.trim() ? args.description.trim() : undefined;
|
|
331
|
+
const provider = typeof args.provider === 'string' && args.provider ? args.provider : undefined;
|
|
332
|
+
if (provider !== undefined && !GIT_PROVIDERS.has(provider)) {
|
|
333
|
+
return { statusCode: 400, status: 'failed', error: 'provider must be "almanak" or "github"' };
|
|
334
|
+
}
|
|
335
|
+
let rootDir;
|
|
336
|
+
try {
|
|
337
|
+
rootDir = normalizeExportRootDir(args.rootDir);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
return { status: 'preflight_failed', message: (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err) };
|
|
341
|
+
}
|
|
342
|
+
// a. A chat links to exactly one repository — never create a second one.
|
|
343
|
+
const linked = await this.getLinkedSessionRepo();
|
|
344
|
+
if (linked)
|
|
345
|
+
return this.alreadyLinkedResult(linked);
|
|
346
|
+
const sessionId = this.client.sessionId();
|
|
347
|
+
if (!sessionId) {
|
|
348
|
+
return { statusCode: 400, status: 'failed', error: 'The platform token carries no agentSessionId claim; this tool only works inside an Almanak Code session.' };
|
|
349
|
+
}
|
|
350
|
+
const cwd = this.workspaceRoot;
|
|
351
|
+
// b. Flatten rootDir into the workspace root so the repository is flat.
|
|
352
|
+
if (rootDir) {
|
|
353
|
+
const flat = await (0, workspace_git_1.flattenRootDir)(cwd, rootDir);
|
|
354
|
+
if (!flat.ok)
|
|
355
|
+
return { status: 'preflight_failed', message: flat.error };
|
|
356
|
+
}
|
|
357
|
+
// c. Something must survive the export skip rules before we create a repository.
|
|
358
|
+
if (!(await (0, workspace_git_1.hasExportableFiles)(cwd))) {
|
|
359
|
+
return {
|
|
360
|
+
status: 'preflight_failed',
|
|
361
|
+
message: 'No exportable files in the workspace (session-internal, env, dependency and cache files are excluded). Create the strategy files first, then retry.',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// d. Create + link the repository (the backend also provisions git credentials for this session).
|
|
365
|
+
const res = await this.client.linkSessionRepository(sessionId, { create: { repoName, description, provider: provider } });
|
|
366
|
+
const failure = this.mapLinkFailure(res);
|
|
367
|
+
if (failure)
|
|
368
|
+
return failure;
|
|
369
|
+
const link = res.data;
|
|
370
|
+
if (link.status === 'already_linked') {
|
|
371
|
+
// Lost a race with another link in this session: surface it, never push to a repository we did not just create.
|
|
372
|
+
return this.alreadyLinkedResult({
|
|
373
|
+
repoOwner: link.repoOwner,
|
|
374
|
+
repoName: link.repoName,
|
|
375
|
+
repoUrl: link.repoUrl,
|
|
376
|
+
provider: (_b = link.provider) !== null && _b !== void 0 ? _b : null,
|
|
377
|
+
strategyLinkId: (_c = link.strategyLinkId) !== null && _c !== void 0 ? _c : null,
|
|
378
|
+
repoId: (_d = link.repoId) !== null && _d !== void 0 ? _d : null,
|
|
379
|
+
cloneUrl: (_e = link.cloneUrl) !== null && _e !== void 0 ? _e : null,
|
|
380
|
+
gitHost: (_f = link.gitHost) !== null && _f !== void 0 ? _f : null,
|
|
381
|
+
defaultBranch: (_g = link.defaultBranch) !== null && _g !== void 0 ? _g : null,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const defaultBranch = link.defaultBranch || 'main';
|
|
385
|
+
const linkedFields = {
|
|
386
|
+
provider: link.provider,
|
|
387
|
+
repoOwner: link.repoOwner,
|
|
388
|
+
repoName: link.repoName,
|
|
389
|
+
repoUrl: link.repoUrl,
|
|
390
|
+
strategyLinkId: link.strategyLinkId,
|
|
391
|
+
cloneUrl: link.cloneUrl,
|
|
392
|
+
};
|
|
393
|
+
// e. git, in the workspace, with ALMANAK_SESSION_ID for the credential helper.
|
|
394
|
+
const opts = this.gitOpts(sessionId);
|
|
395
|
+
let branch = defaultBranch;
|
|
396
|
+
// Always land the export on the repository's default branch: backtests and deploys resolve that
|
|
397
|
+
// branch, so a workspace already on another local branch pushes there via an explicit refspec.
|
|
398
|
+
const pushRefspec = (b) => (b === defaultBranch ? b : `${b}:${defaultBranch}`);
|
|
399
|
+
const pushHint = (b) => `Repository is linked; fix the problem and run: git push -u origin ${pushRefspec(b)}`;
|
|
400
|
+
try {
|
|
401
|
+
if (!(await (0, workspace_git_1.isGitRepo)(cwd))) {
|
|
402
|
+
await (0, workspace_git_1.initRepo)(defaultBranch, opts);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const existing = await (0, workspace_git_1.currentBranch)(opts);
|
|
406
|
+
if (existing) {
|
|
407
|
+
branch = existing;
|
|
408
|
+
}
|
|
409
|
+
else if (!(await (0, workspace_git_1.hasCommits)(opts))) {
|
|
410
|
+
// Unborn HEAD with a different name: rename it before the first commit.
|
|
411
|
+
await (0, workspace_git_1.runGit)(['symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`], opts);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
await (0, workspace_git_1.runGit)(['checkout', '-B', defaultBranch], opts);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
418
|
+
await (0, workspace_git_1.ensureCommitIdentity)(opts);
|
|
419
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
420
|
+
await (0, workspace_git_1.runGit)(['add', '-A'], opts);
|
|
421
|
+
const nothingStaged = await (0, workspace_git_1.gitSucceeds)(['diff', '--cached', '--quiet'], opts);
|
|
422
|
+
if (nothingStaged && !(await (0, workspace_git_1.hasCommits)(opts))) {
|
|
423
|
+
return {
|
|
424
|
+
status: 'preflight_failed',
|
|
425
|
+
...linkedFields,
|
|
426
|
+
message: 'No exportable files: everything in the workspace is ignored by git (.gitignore or the managed excludes). ' +
|
|
427
|
+
`The repository ${link.repoOwner}/${link.repoName} is linked but empty — add files and run: git add -A && git commit -m "..." && git push -u origin ${branch}`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (!nothingStaged)
|
|
431
|
+
await (0, workspace_git_1.runGit)(['commit', '-q', '-m', EXPORT_COMMIT_MESSAGE], opts);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
return this.linkedPushFailed(err, linkedFields, branch, pushHint(branch));
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
await (0, workspace_git_1.runGit)(['push', '-u', 'origin', pushRefspec(branch)], this.gitOpts(sessionId, workspace_git_1.GIT_NETWORK_TIMEOUT_MS));
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return this.linkedPushFailed(err, linkedFields, branch, pushHint(branch));
|
|
441
|
+
}
|
|
442
|
+
const commitSha = (await (0, workspace_git_1.runGit)(['rev-parse', 'HEAD'], opts)).stdout.trim();
|
|
443
|
+
// f. Done — the agent continues in the same turn.
|
|
444
|
+
return {
|
|
445
|
+
status: 'completed',
|
|
446
|
+
provider: link.provider,
|
|
447
|
+
repoOwner: link.repoOwner,
|
|
448
|
+
repoName: link.repoName,
|
|
449
|
+
repoUrl: link.repoUrl,
|
|
450
|
+
strategyLinkId: link.strategyLinkId,
|
|
451
|
+
commitSha,
|
|
452
|
+
branch: defaultBranch,
|
|
453
|
+
...(branch !== defaultBranch ? { localBranch: branch } : {}),
|
|
454
|
+
message: `Exported and pushed ${commitSha.slice(0, 8)} to ${link.repoOwner}/${link.repoName} (${defaultBranch}). ` +
|
|
455
|
+
(branch !== defaultBranch
|
|
456
|
+
? `The workspace stays on its local branch ${branch}, which tracks origin/${defaultBranch}. `
|
|
457
|
+
: '') +
|
|
458
|
+
'The workspace is now a git checkout of this repository.',
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
alreadyLinkedResult(linked) {
|
|
462
|
+
return {
|
|
463
|
+
status: 'already_linked',
|
|
464
|
+
repoOwner: linked.repoOwner,
|
|
465
|
+
repoName: linked.repoName,
|
|
466
|
+
repoUrl: linked.repoUrl,
|
|
467
|
+
strategyLinkId: linked.strategyLinkId,
|
|
468
|
+
provider: linked.provider,
|
|
469
|
+
...(linked.cloneUrl ? { cloneUrl: linked.cloneUrl } : {}),
|
|
470
|
+
...(linked.defaultBranch ? { defaultBranch: linked.defaultBranch } : {}),
|
|
471
|
+
message: `This chat is already linked to ${linked.repoOwner}/${linked.repoName}. ` +
|
|
472
|
+
'Exporting again is not possible from this chat — use git add/commit/push in the workspace to publish further changes.',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
linkedPushFailed(err, linkedFields, branch, hint) {
|
|
476
|
+
var _a;
|
|
477
|
+
const raw = err instanceof workspace_git_1.GitError ? err.message : (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err);
|
|
478
|
+
return {
|
|
479
|
+
status: 'linked_push_failed',
|
|
480
|
+
error: (0, workspace_git_1.redactSecrets)(raw),
|
|
481
|
+
...linkedFields,
|
|
482
|
+
branch,
|
|
483
|
+
message: hint,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* strategies_link_workspace: attach the chat to an existing strategy link and wire the
|
|
488
|
+
* workspace to its repository with git (clone into an empty workspace, or fetch + new branch
|
|
489
|
+
* when user files exist so nothing is overwritten).
|
|
490
|
+
*/
|
|
491
|
+
async linkWorkspace(args) {
|
|
492
|
+
var _a;
|
|
493
|
+
const strategyLinkId = typeof args.strategy_link_id === 'string' ? args.strategy_link_id.trim() : '';
|
|
494
|
+
if (!strategyLinkId)
|
|
495
|
+
return { statusCode: 400, status: 'failed', error: 'strategy_link_id is required' };
|
|
496
|
+
const sessionId = this.client.sessionId();
|
|
497
|
+
if (!sessionId) {
|
|
498
|
+
return { statusCode: 400, status: 'failed', error: 'The platform token carries no agentSessionId claim; this tool only works inside an Almanak Code session.' };
|
|
499
|
+
}
|
|
500
|
+
const cwd = this.workspaceRoot;
|
|
501
|
+
const res = await this.client.linkSessionRepository(sessionId, { strategyLinkId });
|
|
502
|
+
const failure = this.mapLinkFailure(res);
|
|
503
|
+
if (failure)
|
|
504
|
+
return failure;
|
|
505
|
+
const link = res.data;
|
|
506
|
+
const defaultBranch = link.defaultBranch || 'main';
|
|
507
|
+
const base = {
|
|
508
|
+
provider: link.provider,
|
|
509
|
+
repoId: link.repoId,
|
|
510
|
+
repoOwner: link.repoOwner,
|
|
511
|
+
repoName: link.repoName,
|
|
512
|
+
repoUrl: link.repoUrl,
|
|
513
|
+
cloneUrl: link.cloneUrl,
|
|
514
|
+
strategyLinkId: link.strategyLinkId,
|
|
515
|
+
defaultBranch,
|
|
516
|
+
};
|
|
517
|
+
if (link.status === 'already_linked' && link.strategyLinkId && link.strategyLinkId !== strategyLinkId) {
|
|
518
|
+
return {
|
|
519
|
+
status: 'already_linked',
|
|
520
|
+
...base,
|
|
521
|
+
branch: null,
|
|
522
|
+
message: `This chat is already linked to ${link.repoOwner}/${link.repoName} (strategy link ${link.strategyLinkId}); it cannot be relinked to ${strategyLinkId}. Workspace git was left untouched.`,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const opts = this.gitOpts(sessionId);
|
|
526
|
+
const netOpts = this.gitOpts(sessionId, workspace_git_1.GIT_NETWORK_TIMEOUT_MS);
|
|
527
|
+
try {
|
|
528
|
+
if (await (0, workspace_git_1.isGitRepo)(cwd)) {
|
|
529
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
530
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
531
|
+
const branch = await (0, workspace_git_1.currentBranch)(opts);
|
|
532
|
+
return {
|
|
533
|
+
status: link.status,
|
|
534
|
+
...base,
|
|
535
|
+
branch,
|
|
536
|
+
message: `Linked ${link.repoOwner}/${link.repoName}; the workspace was already a git repository, so only the origin remote was repointed to it. Run git fetch origin and reconcile with origin/${defaultBranch} as needed.`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const userFiles = await (0, workspace_git_1.hasUserFiles)(cwd);
|
|
540
|
+
await (0, workspace_git_1.initRepo)(defaultBranch, opts);
|
|
541
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
542
|
+
await (0, workspace_git_1.ensureCommitIdentity)(opts);
|
|
543
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
544
|
+
await (0, workspace_git_1.runGit)(['fetch', '-q', 'origin'], netOpts);
|
|
545
|
+
const remoteHasBranch = await (0, workspace_git_1.remoteBranchExists)(defaultBranch, opts);
|
|
546
|
+
if (!userFiles) {
|
|
547
|
+
if (remoteHasBranch) {
|
|
548
|
+
await (0, workspace_git_1.runGit)(['checkout', '-q', '-B', defaultBranch, `origin/${defaultBranch}`], opts);
|
|
549
|
+
return {
|
|
550
|
+
status: link.status,
|
|
551
|
+
...base,
|
|
552
|
+
branch: defaultBranch,
|
|
553
|
+
message: `Linked ${link.repoOwner}/${link.repoName} and checked out origin/${defaultBranch} into the workspace.`,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
status: link.status,
|
|
558
|
+
...base,
|
|
559
|
+
branch: defaultBranch,
|
|
560
|
+
message: `Linked ${link.repoOwner}/${link.repoName}; the repository has no ${defaultBranch} branch yet, so the workspace is an empty checkout on ${defaultBranch}. Add files, commit, and git push -u origin ${defaultBranch}.`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const sessionBranch = `almanak/session-${sessionId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8)}`;
|
|
564
|
+
await (0, workspace_git_1.runGit)(['symbolic-ref', 'HEAD', `refs/heads/${sessionBranch}`], opts);
|
|
565
|
+
return {
|
|
566
|
+
status: link.status,
|
|
567
|
+
...base,
|
|
568
|
+
branch: sessionBranch,
|
|
569
|
+
message: `Linked ${link.repoOwner}/${link.repoName}. The workspace already had files, so they were NOT overwritten: git was initialised on new branch ${sessionBranch} ` +
|
|
570
|
+
`with the existing files left as uncommitted changes` +
|
|
571
|
+
(remoteHasBranch
|
|
572
|
+
? ` and origin/${defaultBranch} fetched. Reconcile with git diff origin/${defaultBranch}, then commit and push (git push -u origin ${sessionBranch}, or merge into ${defaultBranch}).`
|
|
573
|
+
: `; the repository has no ${defaultBranch} branch yet. Commit and git push -u origin ${defaultBranch} when ready.`),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
const raw = err instanceof workspace_git_1.GitError ? err.message : (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err);
|
|
578
|
+
return {
|
|
579
|
+
status: 'linked_attach_failed',
|
|
580
|
+
error: (0, workspace_git_1.redactSecrets)(raw),
|
|
581
|
+
...base,
|
|
582
|
+
branch: null,
|
|
583
|
+
message: `The chat is linked to ${link.repoOwner}/${link.repoName} but attaching the workspace with git failed; fix the problem and run git remote add origin ${link.cloneUrl} && git fetch origin manually.`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
280
587
|
register(mode, definition, handler) {
|
|
281
588
|
this.registeredNames.add(definition.name);
|
|
282
589
|
if (!this.modeEnabled(mode))
|
|
@@ -330,7 +637,7 @@ class PlatformToolHandler {
|
|
|
330
637
|
type: 'object',
|
|
331
638
|
properties: {
|
|
332
639
|
agent_name: { type: 'string', description: 'Display name for the agent' },
|
|
333
|
-
github_strategy_link_id: { type: 'string', description: 'ID of the linked strategy (from strategies_list or the
|
|
640
|
+
github_strategy_link_id: { type: 'string', description: 'ID of the linked strategy (from strategies_list or the strategies_export_workspace result)' },
|
|
334
641
|
github_ref: { type: 'string', description: 'Git branch, tag, or commit SHA to deploy' },
|
|
335
642
|
github_ref_type: {
|
|
336
643
|
type: 'string',
|
|
@@ -824,7 +1131,7 @@ class PlatformToolHandler {
|
|
|
824
1131
|
});
|
|
825
1132
|
this.register('strategies', {
|
|
826
1133
|
name: 'strategies_supported_protocols',
|
|
827
|
-
description: 'List the DeFi protocols the Almanak platform supports running strategies against. Use it to confirm a protocol a strategy depends on is supported before deploying. Pass chain_id to filter to protocols supported on a specific chain. Returns each protocol\'s key, name, category, supported chain_ids, and whether it requires offchain/API integration.',
|
|
1134
|
+
description: 'List the DeFi protocols the Almanak platform supports running strategies against. Use it to confirm a protocol a strategy depends on is supported before deploying. Pass chain_id to filter to protocols supported on a specific chain. Returns each protocol\'s key, name, category, supported chain_ids, and whether it requires offchain/API integration. When the session runs a specific SDK generation (ALMANAK_SDK_GENERATION), the result is scoped to the protocols and chains that generation supports.',
|
|
828
1135
|
inputSchema: {
|
|
829
1136
|
type: 'object',
|
|
830
1137
|
properties: {
|
|
@@ -835,6 +1142,7 @@ class PlatformToolHandler {
|
|
|
835
1142
|
}, (args) => this.client.getSupportedProtocols({
|
|
836
1143
|
chainId: args.chain_id,
|
|
837
1144
|
includeInactive: args.include_inactive === true,
|
|
1145
|
+
sdkGeneration: this.options.sdkGeneration,
|
|
838
1146
|
}));
|
|
839
1147
|
this.register('strategies', {
|
|
840
1148
|
name: 'strategies_validate',
|
|
@@ -861,92 +1169,60 @@ class PlatformToolHandler {
|
|
|
861
1169
|
return this.client.validateRepository(args.owner, args.repo, ref);
|
|
862
1170
|
});
|
|
863
1171
|
this.register('strategies', {
|
|
864
|
-
name: '
|
|
865
|
-
description: '
|
|
1172
|
+
name: 'strategies_export_workspace',
|
|
1173
|
+
description: 'Export the current workspace to a NEW git repository and link it to this chat, immediately, inside this tool call: ' +
|
|
1174
|
+
'the platform creates and links the repository, then this tool runs git init/add/commit/push in the workspace itself. ' +
|
|
1175
|
+
'No widget is shown and no user click is needed — on status "completed" the repository exists with the pushed commit, so ' +
|
|
1176
|
+
'continue in the SAME turn (for example run a backtest or deploy). Pass repository metadata only; never read or submit files. ' +
|
|
1177
|
+
'Session-internal files (.almanak, .almanakdb, almanak_state.db*, .env, .venv, node_modules, .logs) are excluded automatically. ' +
|
|
1178
|
+
'Pass rootDir only when the strategy lives in a subfolder (e.g. "my_strategy/"): its contents are moved up to the workspace root ' +
|
|
1179
|
+
'so the repository is flat. Statuses: "completed" (done: repoUrl, strategyLinkId, commitSha, branch); "already_linked" (this chat ' +
|
|
1180
|
+
'already has a repository — do not export again, use git add/commit/push in the workspace); "name_conflict" (pick a different ' +
|
|
1181
|
+
'repoName and retry); "preflight_failed" (fix the workspace/rootDir and retry); "github_connection_required", "provider_disabled", ' +
|
|
1182
|
+
'"in_progress" (report to the user); "linked_push_failed" (the repository IS linked but the commit/push failed — fix the git ' +
|
|
1183
|
+
'problem and run the git command in the message; do not call this tool again). Never claim the export succeeded before "completed".',
|
|
866
1184
|
inputSchema: {
|
|
867
1185
|
type: 'object',
|
|
868
1186
|
properties: {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1187
|
+
repoName: { type: 'string', description: 'Repository name (required). Lowercase letters, digits, dashes recommended.' },
|
|
1188
|
+
description: { type: 'string', description: 'Repository description.' },
|
|
1189
|
+
rootDir: {
|
|
1190
|
+
type: 'string',
|
|
1191
|
+
description: 'Optional workspace-relative strategy folder whose contents become the repository root (flattened into the workspace). ' +
|
|
1192
|
+
'Omit for strategies at the workspace root (the default). Never pass an absolute path.',
|
|
1193
|
+
},
|
|
1194
|
+
provider: {
|
|
1195
|
+
type: 'string',
|
|
1196
|
+
enum: ['almanak', 'github'],
|
|
1197
|
+
description: 'Git host. Default: the platform default (Almanak Git). "github" requires the user to have GitHub connected.',
|
|
1198
|
+
},
|
|
873
1199
|
},
|
|
874
|
-
required: [],
|
|
1200
|
+
required: ['repoName'],
|
|
875
1201
|
},
|
|
876
|
-
},
|
|
877
|
-
var _a;
|
|
878
|
-
// A chat session links to exactly one repository. Opening the export widget on a session
|
|
879
|
-
// that is already linked can only end in a conflict — and, before the pre-check landed, a
|
|
880
|
-
// throwaway repo on the user's account (ALM-3262). Hand back the existing link instead.
|
|
881
|
-
const linkedRepo = await this.getLinkedSessionRepo();
|
|
882
|
-
if (linkedRepo) {
|
|
883
|
-
return {
|
|
884
|
-
show_widget: false,
|
|
885
|
-
already_linked: true,
|
|
886
|
-
...linkedRepo,
|
|
887
|
-
message: `This chat is already linked to ${linkedRepo.repoOwner}/${linkedRepo.repoName}. ` +
|
|
888
|
-
'Exporting again is not possible from this chat — push further changes with git, or start a new chat to export to a different repository.',
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
const result = {
|
|
892
|
-
show_widget: true,
|
|
893
|
-
strategyName: (_a = args.strategyName) !== null && _a !== void 0 ? _a : 'Strategy',
|
|
894
|
-
};
|
|
895
|
-
if (typeof args.repoName === 'string' && args.repoName.trim())
|
|
896
|
-
result.name = args.repoName.trim();
|
|
897
|
-
if (typeof args.description === 'string' && args.description.trim())
|
|
898
|
-
result.description = args.description.trim();
|
|
899
|
-
const rootDir = normalizeExportRootDir(args.rootDir);
|
|
900
|
-
if (rootDir)
|
|
901
|
-
result.rootDir = rootDir;
|
|
902
|
-
return result;
|
|
903
|
-
});
|
|
1202
|
+
}, (args) => this.exportWorkspace(args));
|
|
904
1203
|
this.register('strategies', {
|
|
905
|
-
name: '
|
|
906
|
-
description: '
|
|
1204
|
+
name: 'strategies_link_workspace',
|
|
1205
|
+
description: 'Link an EXISTING strategy repository (a strategy_link_id from strategies_list) to this chat and attach the workspace to it with git, ' +
|
|
1206
|
+
'immediately, inside this tool call. Use it when the user wants to continue work on a strategy they already exported. ' +
|
|
1207
|
+
'An empty workspace is checked out from the repository default branch. A workspace that already has files is NOT overwritten: ' +
|
|
1208
|
+
'the tool initialises git, fetches the repository, and leaves the files as uncommitted changes on a new branch named in the result — ' +
|
|
1209
|
+
'inspect git status / git diff origin/<default branch>, reconcile, then commit and push. If the workspace is already a git ' +
|
|
1210
|
+
'repository, only the origin remote is repointed. Statuses: "linked", "already_linked" (with the linked repo), "failed". ' +
|
|
1211
|
+
'The chat can be linked to exactly one repository; to export a brand new one use strategies_export_workspace instead.',
|
|
907
1212
|
inputSchema: {
|
|
908
1213
|
type: 'object',
|
|
909
1214
|
properties: {
|
|
910
|
-
|
|
911
|
-
type: 'array',
|
|
912
|
-
description: 'Array of files to export with explicit repo-relative paths.',
|
|
913
|
-
items: {
|
|
914
|
-
type: 'object',
|
|
915
|
-
properties: {
|
|
916
|
-
path: { type: 'string', description: 'Relative file path in the repo (e.g. "src/strategy.ts").' },
|
|
917
|
-
content: { type: 'string', description: 'File content.' },
|
|
918
|
-
encoding: { type: 'string', enum: ['utf-8', 'base64'], description: 'Content encoding. Default: "utf-8".' },
|
|
919
|
-
},
|
|
920
|
-
required: ['path', 'content'],
|
|
921
|
-
},
|
|
922
|
-
},
|
|
923
|
-
repoName: { type: 'string', description: 'Repository name (required).' },
|
|
924
|
-
description: { type: 'string', description: 'Repository description.' },
|
|
925
|
-
isPrivate: { type: 'boolean', description: 'Whether the new repo should be private. Default: true.' },
|
|
1215
|
+
strategy_link_id: { type: 'string', description: 'The strategy link ID (from strategies_list).' },
|
|
926
1216
|
},
|
|
927
|
-
required: ['
|
|
1217
|
+
required: ['strategy_link_id'],
|
|
928
1218
|
},
|
|
929
|
-
},
|
|
930
|
-
var _a;
|
|
931
|
-
const files = args.files;
|
|
932
|
-
if (!files || files.length === 0) {
|
|
933
|
-
return { statusCode: 400, error: 'files array is required' };
|
|
934
|
-
}
|
|
935
|
-
if (!((_a = args.repoName) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
936
|
-
return { statusCode: 400, error: 'repoName is required' };
|
|
937
|
-
}
|
|
938
|
-
return this.client.exportToGitHub({
|
|
939
|
-
files,
|
|
940
|
-
repoName: args.repoName,
|
|
941
|
-
description: args.description,
|
|
942
|
-
isPrivate: args.isPrivate,
|
|
943
|
-
});
|
|
944
|
-
});
|
|
1219
|
+
}, (args) => this.linkWorkspace(args));
|
|
945
1220
|
this.register('strategies', {
|
|
946
1221
|
name: 'backtests_run',
|
|
947
|
-
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is ' +
|
|
948
|
-
'
|
|
949
|
-
'push first and re-run after every code change you want tested.
|
|
1222
|
+
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is in its ' +
|
|
1223
|
+
'repository (strategies_export_workspace, or git push on a linked chat) and before deploying — backtests run against a ' +
|
|
1224
|
+
'pushed commit, never local files, so export/push first and re-run after every code change you want tested. ' +
|
|
1225
|
+
'The call submits the run, renders a live backtest ' +
|
|
950
1226
|
'widget in the chat, and PAUSES until the run finishes (typically 1-5 minutes); it then resumes you with the ' +
|
|
951
1227
|
'result summary (total return, Sharpe, max drawdown, win rate, trades, and the run_validity verdict) merged into ' +
|
|
952
1228
|
'this tool result as _widgetResponse — or the failure reason if the run failed. A run whose verdict is INVALID or ' +
|
|
@@ -964,7 +1240,7 @@ class PlatformToolHandler {
|
|
|
964
1240
|
properties: {
|
|
965
1241
|
strategy_link_id: {
|
|
966
1242
|
type: 'string',
|
|
967
|
-
description: 'The strategy link ID (from strategies_list or the
|
|
1243
|
+
description: 'The strategy link ID (from strategies_list or the strategies_export_workspace result).',
|
|
968
1244
|
},
|
|
969
1245
|
start_time: {
|
|
970
1246
|
type: 'string',
|
|
@@ -1343,7 +1619,7 @@ class PlatformToolHandler {
|
|
|
1343
1619
|
if (!strategyLinkId) {
|
|
1344
1620
|
return {
|
|
1345
1621
|
statusCode: 400,
|
|
1346
|
-
error: 'strategy_link_id is required — pass the linked strategy id (from strategies_list or the
|
|
1622
|
+
error: 'strategy_link_id is required — pass the linked strategy id (from strategies_list or the strategies_export_workspace result).',
|
|
1347
1623
|
backtest_config: { start_time: startTime, end_time: String((_c = args.end_time) !== null && _c !== void 0 ? _c : '').trim(), include_gas_costs: args.include_gas_costs !== false },
|
|
1348
1624
|
};
|
|
1349
1625
|
}
|