@almanak/mcp-server 0.2.1 → 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 +65 -11
- package/dist/client.js +31 -13
- package/dist/client.js.map +1 -1
- package/dist/testing/__tests__/platform-client.test.js +48 -0
- package/dist/testing/__tests__/platform-client.test.js.map +1 -1
- 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 +28 -0
- package/dist/tools.js +354 -80
- 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\/[^/]+\/?$/;
|
|
@@ -196,6 +199,7 @@ class PlatformToolHandler {
|
|
|
196
199
|
disabledTools: [],
|
|
197
200
|
sdkGeneration: undefined,
|
|
198
201
|
testController: null,
|
|
202
|
+
workspaceRoot: undefined,
|
|
199
203
|
...options,
|
|
200
204
|
};
|
|
201
205
|
this.enabledModes = new Set(this.options.modes);
|
|
@@ -255,7 +259,7 @@ class PlatformToolHandler {
|
|
|
255
259
|
* is unavailable. Best effort by design: a failed check must never block the export flow.
|
|
256
260
|
*/
|
|
257
261
|
async getLinkedSessionRepo() {
|
|
258
|
-
var _a;
|
|
262
|
+
var _a, _b;
|
|
259
263
|
try {
|
|
260
264
|
const response = await this.client.getSessionRepoLink();
|
|
261
265
|
const data = ((_a = response === null || response === void 0 ? void 0 : response.data) !== null && _a !== void 0 ? _a : response);
|
|
@@ -265,19 +269,321 @@ class PlatformToolHandler {
|
|
|
265
269
|
const repoName = typeof data.repoName === 'string' ? data.repoName : '';
|
|
266
270
|
if (!repoOwner || !repoName)
|
|
267
271
|
return null;
|
|
272
|
+
const str = (key) => (typeof data[key] === 'string' ? data[key] : null);
|
|
268
273
|
return {
|
|
269
274
|
repoOwner,
|
|
270
275
|
repoName,
|
|
271
276
|
// The backend URL is provider-aware (Almanak Git or GitHub) — never assume a github.com host here.
|
|
272
|
-
repoUrl:
|
|
273
|
-
provider:
|
|
274
|
-
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'),
|
|
275
284
|
};
|
|
276
285
|
}
|
|
277
286
|
catch {
|
|
278
287
|
return null;
|
|
279
288
|
}
|
|
280
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
|
+
}
|
|
281
587
|
register(mode, definition, handler) {
|
|
282
588
|
this.registeredNames.add(definition.name);
|
|
283
589
|
if (!this.modeEnabled(mode))
|
|
@@ -331,7 +637,7 @@ class PlatformToolHandler {
|
|
|
331
637
|
type: 'object',
|
|
332
638
|
properties: {
|
|
333
639
|
agent_name: { type: 'string', description: 'Display name for the agent' },
|
|
334
|
-
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)' },
|
|
335
641
|
github_ref: { type: 'string', description: 'Git branch, tag, or commit SHA to deploy' },
|
|
336
642
|
github_ref_type: {
|
|
337
643
|
type: 'string',
|
|
@@ -863,92 +1169,60 @@ class PlatformToolHandler {
|
|
|
863
1169
|
return this.client.validateRepository(args.owner, args.repo, ref);
|
|
864
1170
|
});
|
|
865
1171
|
this.register('strategies', {
|
|
866
|
-
name: '
|
|
867
|
-
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".',
|
|
868
1184
|
inputSchema: {
|
|
869
1185
|
type: 'object',
|
|
870
1186
|
properties: {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
+
},
|
|
875
1199
|
},
|
|
876
|
-
required: [],
|
|
1200
|
+
required: ['repoName'],
|
|
877
1201
|
},
|
|
878
|
-
},
|
|
879
|
-
var _a;
|
|
880
|
-
// A chat session links to exactly one repository. Opening the export widget on a session
|
|
881
|
-
// that is already linked can only end in a conflict — and, before the pre-check landed, a
|
|
882
|
-
// throwaway repo on the user's account (ALM-3262). Hand back the existing link instead.
|
|
883
|
-
const linkedRepo = await this.getLinkedSessionRepo();
|
|
884
|
-
if (linkedRepo) {
|
|
885
|
-
return {
|
|
886
|
-
show_widget: false,
|
|
887
|
-
already_linked: true,
|
|
888
|
-
...linkedRepo,
|
|
889
|
-
message: `This chat is already linked to ${linkedRepo.repoOwner}/${linkedRepo.repoName}. ` +
|
|
890
|
-
'Exporting again is not possible from this chat — push further changes with git, or start a new chat to export to a different repository.',
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
const result = {
|
|
894
|
-
show_widget: true,
|
|
895
|
-
strategyName: (_a = args.strategyName) !== null && _a !== void 0 ? _a : 'Strategy',
|
|
896
|
-
};
|
|
897
|
-
if (typeof args.repoName === 'string' && args.repoName.trim())
|
|
898
|
-
result.name = args.repoName.trim();
|
|
899
|
-
if (typeof args.description === 'string' && args.description.trim())
|
|
900
|
-
result.description = args.description.trim();
|
|
901
|
-
const rootDir = normalizeExportRootDir(args.rootDir);
|
|
902
|
-
if (rootDir)
|
|
903
|
-
result.rootDir = rootDir;
|
|
904
|
-
return result;
|
|
905
|
-
});
|
|
1202
|
+
}, (args) => this.exportWorkspace(args));
|
|
906
1203
|
this.register('strategies', {
|
|
907
|
-
name: '
|
|
908
|
-
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.',
|
|
909
1212
|
inputSchema: {
|
|
910
1213
|
type: 'object',
|
|
911
1214
|
properties: {
|
|
912
|
-
|
|
913
|
-
type: 'array',
|
|
914
|
-
description: 'Array of files to export with explicit repo-relative paths.',
|
|
915
|
-
items: {
|
|
916
|
-
type: 'object',
|
|
917
|
-
properties: {
|
|
918
|
-
path: { type: 'string', description: 'Relative file path in the repo (e.g. "src/strategy.ts").' },
|
|
919
|
-
content: { type: 'string', description: 'File content.' },
|
|
920
|
-
encoding: { type: 'string', enum: ['utf-8', 'base64'], description: 'Content encoding. Default: "utf-8".' },
|
|
921
|
-
},
|
|
922
|
-
required: ['path', 'content'],
|
|
923
|
-
},
|
|
924
|
-
},
|
|
925
|
-
repoName: { type: 'string', description: 'Repository name (required).' },
|
|
926
|
-
description: { type: 'string', description: 'Repository description.' },
|
|
927
|
-
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).' },
|
|
928
1216
|
},
|
|
929
|
-
required: ['
|
|
1217
|
+
required: ['strategy_link_id'],
|
|
930
1218
|
},
|
|
931
|
-
},
|
|
932
|
-
var _a;
|
|
933
|
-
const files = args.files;
|
|
934
|
-
if (!files || files.length === 0) {
|
|
935
|
-
return { statusCode: 400, error: 'files array is required' };
|
|
936
|
-
}
|
|
937
|
-
if (!((_a = args.repoName) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
938
|
-
return { statusCode: 400, error: 'repoName is required' };
|
|
939
|
-
}
|
|
940
|
-
return this.client.exportToGitHub({
|
|
941
|
-
files,
|
|
942
|
-
repoName: args.repoName,
|
|
943
|
-
description: args.description,
|
|
944
|
-
isPrivate: args.isPrivate,
|
|
945
|
-
});
|
|
946
|
-
});
|
|
1219
|
+
}, (args) => this.linkWorkspace(args));
|
|
947
1220
|
this.register('strategies', {
|
|
948
1221
|
name: 'backtests_run',
|
|
949
|
-
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is ' +
|
|
950
|
-
'
|
|
951
|
-
'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 ' +
|
|
952
1226
|
'widget in the chat, and PAUSES until the run finishes (typically 1-5 minutes); it then resumes you with the ' +
|
|
953
1227
|
'result summary (total return, Sharpe, max drawdown, win rate, trades, and the run_validity verdict) merged into ' +
|
|
954
1228
|
'this tool result as _widgetResponse — or the failure reason if the run failed. A run whose verdict is INVALID or ' +
|
|
@@ -966,7 +1240,7 @@ class PlatformToolHandler {
|
|
|
966
1240
|
properties: {
|
|
967
1241
|
strategy_link_id: {
|
|
968
1242
|
type: 'string',
|
|
969
|
-
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).',
|
|
970
1244
|
},
|
|
971
1245
|
start_time: {
|
|
972
1246
|
type: 'string',
|
|
@@ -1345,7 +1619,7 @@ class PlatformToolHandler {
|
|
|
1345
1619
|
if (!strategyLinkId) {
|
|
1346
1620
|
return {
|
|
1347
1621
|
statusCode: 400,
|
|
1348
|
-
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).',
|
|
1349
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 },
|
|
1350
1624
|
};
|
|
1351
1625
|
}
|