@almanak/mcp-server 0.2.1 → 0.2.3
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 +64 -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 +345 -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,8 @@ 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 EXPORT_COMMIT_MESSAGE = 'feat: initial strategy export from Almanak';
|
|
52
54
|
const ROOT_DIR_ALLOWED_RE = /^[a-zA-Z0-9._/-]+$/;
|
|
53
55
|
const WORKSPACE_ABSOLUTE_PATH_RE = /^\/(?:tmp\/)?workspaces\/[^/]+\/(.+)$/;
|
|
54
56
|
const WORKSPACE_ROOT_PATH_RE = /^\/(?:tmp\/)?workspaces\/[^/]+\/?$/;
|
|
@@ -196,6 +198,7 @@ class PlatformToolHandler {
|
|
|
196
198
|
disabledTools: [],
|
|
197
199
|
sdkGeneration: undefined,
|
|
198
200
|
testController: null,
|
|
201
|
+
workspaceRoot: undefined,
|
|
199
202
|
...options,
|
|
200
203
|
};
|
|
201
204
|
this.enabledModes = new Set(this.options.modes);
|
|
@@ -255,7 +258,7 @@ class PlatformToolHandler {
|
|
|
255
258
|
* is unavailable. Best effort by design: a failed check must never block the export flow.
|
|
256
259
|
*/
|
|
257
260
|
async getLinkedSessionRepo() {
|
|
258
|
-
var _a;
|
|
261
|
+
var _a, _b;
|
|
259
262
|
try {
|
|
260
263
|
const response = await this.client.getSessionRepoLink();
|
|
261
264
|
const data = ((_a = response === null || response === void 0 ? void 0 : response.data) !== null && _a !== void 0 ? _a : response);
|
|
@@ -265,19 +268,317 @@ class PlatformToolHandler {
|
|
|
265
268
|
const repoName = typeof data.repoName === 'string' ? data.repoName : '';
|
|
266
269
|
if (!repoOwner || !repoName)
|
|
267
270
|
return null;
|
|
271
|
+
const str = (key) => (typeof data[key] === 'string' ? data[key] : null);
|
|
268
272
|
return {
|
|
269
273
|
repoOwner,
|
|
270
274
|
repoName,
|
|
271
275
|
// The backend URL is provider-aware (Almanak Git or GitHub) — never assume a github.com host here.
|
|
272
|
-
repoUrl:
|
|
273
|
-
provider:
|
|
274
|
-
strategyLinkId:
|
|
276
|
+
repoUrl: (_b = str('repoUrl')) !== null && _b !== void 0 ? _b : '',
|
|
277
|
+
provider: str('provider'),
|
|
278
|
+
strategyLinkId: str('strategyLinkId'),
|
|
279
|
+
repoId: str('repoId'),
|
|
280
|
+
cloneUrl: str('cloneUrl'),
|
|
281
|
+
gitHost: str('gitHost'),
|
|
282
|
+
defaultBranch: str('defaultBranch'),
|
|
275
283
|
};
|
|
276
284
|
}
|
|
277
285
|
catch {
|
|
278
286
|
return null;
|
|
279
287
|
}
|
|
280
288
|
}
|
|
289
|
+
/** Workspace root for git-backed tools (see ToolHandlerOptions.workspaceRoot). */
|
|
290
|
+
get workspaceRoot() {
|
|
291
|
+
var _a;
|
|
292
|
+
return (_a = this.options.workspaceRoot) !== null && _a !== void 0 ? _a : process.cwd();
|
|
293
|
+
}
|
|
294
|
+
gitOpts(sessionId, timeoutMs) {
|
|
295
|
+
return { cwd: this.workspaceRoot, sessionId, ...(timeoutMs !== undefined ? { timeoutMs } : {}) };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Map a failed `POST /agent-sessions/{id}/repository` response to the tool's status vocabulary.
|
|
299
|
+
* Returns null when the response is a success.
|
|
300
|
+
*/
|
|
301
|
+
mapLinkFailure(res) {
|
|
302
|
+
if (res && res.valid !== false && !(typeof res.statusCode === 'number' && res.statusCode >= 400) && res.data)
|
|
303
|
+
return null;
|
|
304
|
+
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';
|
|
305
|
+
const code = typeof (res === null || res === void 0 ? void 0 : res.errorCode) === 'string' ? res.errorCode : null;
|
|
306
|
+
switch (code) {
|
|
307
|
+
case 'name_conflict':
|
|
308
|
+
return { status: 'name_conflict', message: `${message} Choose a different repoName and call the tool again.` };
|
|
309
|
+
case 'github_connection_required':
|
|
310
|
+
return { status: 'github_connection_required', message };
|
|
311
|
+
case 'provider_disabled':
|
|
312
|
+
return { status: 'provider_disabled', message };
|
|
313
|
+
case 'repo_config_in_progress':
|
|
314
|
+
return { status: 'in_progress', message: `${message} Another link operation is still running for this session; wait and retry.` };
|
|
315
|
+
default:
|
|
316
|
+
return { status: 'failed', statusCode: typeof (res === null || res === void 0 ? void 0 : res.statusCode) === 'number' ? res.statusCode : 502, error: message, ...(code ? { errorCode: code } : {}) };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* strategies_export_workspace: create + link a new repository, then init/commit/push the
|
|
321
|
+
* workspace from inside this call. Every step is synchronous so the agent can continue in
|
|
322
|
+
* the same turn on `completed`.
|
|
323
|
+
*/
|
|
324
|
+
async exportWorkspace(args) {
|
|
325
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
326
|
+
const repoName = typeof args.repoName === 'string' ? args.repoName.trim() : '';
|
|
327
|
+
if (!repoName)
|
|
328
|
+
return { statusCode: 400, status: 'failed', error: 'repoName is required' };
|
|
329
|
+
const description = typeof args.description === 'string' && args.description.trim() ? args.description.trim() : undefined;
|
|
330
|
+
let rootDir;
|
|
331
|
+
try {
|
|
332
|
+
rootDir = normalizeExportRootDir(args.rootDir);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return { status: 'preflight_failed', message: (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err) };
|
|
336
|
+
}
|
|
337
|
+
// a. A chat links to exactly one repository — never create a second one.
|
|
338
|
+
const linked = await this.getLinkedSessionRepo();
|
|
339
|
+
if (linked)
|
|
340
|
+
return this.alreadyLinkedResult(linked);
|
|
341
|
+
const sessionId = this.client.sessionId();
|
|
342
|
+
if (!sessionId) {
|
|
343
|
+
return { statusCode: 400, status: 'failed', error: 'The platform token carries no agentSessionId claim; this tool only works inside an Almanak Code session.' };
|
|
344
|
+
}
|
|
345
|
+
const cwd = this.workspaceRoot;
|
|
346
|
+
// b. Flatten rootDir into the workspace root so the repository is flat.
|
|
347
|
+
if (rootDir) {
|
|
348
|
+
const flat = await (0, workspace_git_1.flattenRootDir)(cwd, rootDir);
|
|
349
|
+
if (!flat.ok)
|
|
350
|
+
return { status: 'preflight_failed', message: flat.error };
|
|
351
|
+
}
|
|
352
|
+
// c. Something must survive the export skip rules before we create a repository.
|
|
353
|
+
if (!(await (0, workspace_git_1.hasExportableFiles)(cwd))) {
|
|
354
|
+
return {
|
|
355
|
+
status: 'preflight_failed',
|
|
356
|
+
message: 'No exportable files in the workspace (session-internal, env, dependency and cache files are excluded). Create the strategy files first, then retry.',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// d. Create + link the repository (the backend also provisions git credentials for this session).
|
|
360
|
+
const res = await this.client.linkSessionRepository(sessionId, { create: { repoName, description } });
|
|
361
|
+
const failure = this.mapLinkFailure(res);
|
|
362
|
+
if (failure)
|
|
363
|
+
return failure;
|
|
364
|
+
const link = res.data;
|
|
365
|
+
if (link.status === 'already_linked') {
|
|
366
|
+
// Lost a race with another link in this session: surface it, never push to a repository we did not just create.
|
|
367
|
+
return this.alreadyLinkedResult({
|
|
368
|
+
repoOwner: link.repoOwner,
|
|
369
|
+
repoName: link.repoName,
|
|
370
|
+
repoUrl: link.repoUrl,
|
|
371
|
+
provider: (_b = link.provider) !== null && _b !== void 0 ? _b : null,
|
|
372
|
+
strategyLinkId: (_c = link.strategyLinkId) !== null && _c !== void 0 ? _c : null,
|
|
373
|
+
repoId: (_d = link.repoId) !== null && _d !== void 0 ? _d : null,
|
|
374
|
+
cloneUrl: (_e = link.cloneUrl) !== null && _e !== void 0 ? _e : null,
|
|
375
|
+
gitHost: (_f = link.gitHost) !== null && _f !== void 0 ? _f : null,
|
|
376
|
+
defaultBranch: (_g = link.defaultBranch) !== null && _g !== void 0 ? _g : null,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const defaultBranch = link.defaultBranch || 'main';
|
|
380
|
+
const linkedFields = {
|
|
381
|
+
provider: link.provider,
|
|
382
|
+
repoOwner: link.repoOwner,
|
|
383
|
+
repoName: link.repoName,
|
|
384
|
+
repoUrl: link.repoUrl,
|
|
385
|
+
strategyLinkId: link.strategyLinkId,
|
|
386
|
+
cloneUrl: link.cloneUrl,
|
|
387
|
+
};
|
|
388
|
+
// e. git, in the workspace, with ALMANAK_SESSION_ID for the credential helper.
|
|
389
|
+
const opts = this.gitOpts(sessionId);
|
|
390
|
+
let branch = defaultBranch;
|
|
391
|
+
// Always land the export on the repository's default branch: backtests and deploys resolve that
|
|
392
|
+
// branch, so a workspace already on another local branch pushes there via an explicit refspec.
|
|
393
|
+
const pushRefspec = (b) => (b === defaultBranch ? b : `${b}:${defaultBranch}`);
|
|
394
|
+
const pushHint = (b) => `Repository is linked; fix the problem and run: git push -u origin ${pushRefspec(b)}`;
|
|
395
|
+
try {
|
|
396
|
+
if (!(await (0, workspace_git_1.isGitRepo)(cwd))) {
|
|
397
|
+
await (0, workspace_git_1.initRepo)(defaultBranch, opts);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const existing = await (0, workspace_git_1.currentBranch)(opts);
|
|
401
|
+
if (existing) {
|
|
402
|
+
branch = existing;
|
|
403
|
+
}
|
|
404
|
+
else if (!(await (0, workspace_git_1.hasCommits)(opts))) {
|
|
405
|
+
// Unborn HEAD with a different name: rename it before the first commit.
|
|
406
|
+
await (0, workspace_git_1.runGit)(['symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`], opts);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
await (0, workspace_git_1.runGit)(['checkout', '-B', defaultBranch], opts);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
413
|
+
await (0, workspace_git_1.ensureCommitIdentity)(opts);
|
|
414
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
415
|
+
await (0, workspace_git_1.runGit)(['add', '-A'], opts);
|
|
416
|
+
const nothingStaged = await (0, workspace_git_1.gitSucceeds)(['diff', '--cached', '--quiet'], opts);
|
|
417
|
+
if (nothingStaged && !(await (0, workspace_git_1.hasCommits)(opts))) {
|
|
418
|
+
return {
|
|
419
|
+
status: 'preflight_failed',
|
|
420
|
+
...linkedFields,
|
|
421
|
+
message: 'No exportable files: everything in the workspace is ignored by git (.gitignore or the managed excludes). ' +
|
|
422
|
+
`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}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (!nothingStaged)
|
|
426
|
+
await (0, workspace_git_1.runGit)(['commit', '-q', '-m', EXPORT_COMMIT_MESSAGE], opts);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
return this.linkedPushFailed(err, linkedFields, branch, pushHint(branch));
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await (0, workspace_git_1.runGit)(['push', '-u', 'origin', pushRefspec(branch)], this.gitOpts(sessionId, workspace_git_1.GIT_NETWORK_TIMEOUT_MS));
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return this.linkedPushFailed(err, linkedFields, branch, pushHint(branch));
|
|
436
|
+
}
|
|
437
|
+
const commitSha = (await (0, workspace_git_1.runGit)(['rev-parse', 'HEAD'], opts)).stdout.trim();
|
|
438
|
+
// f. Done — the agent continues in the same turn.
|
|
439
|
+
return {
|
|
440
|
+
status: 'completed',
|
|
441
|
+
provider: link.provider,
|
|
442
|
+
repoOwner: link.repoOwner,
|
|
443
|
+
repoName: link.repoName,
|
|
444
|
+
repoUrl: link.repoUrl,
|
|
445
|
+
strategyLinkId: link.strategyLinkId,
|
|
446
|
+
commitSha,
|
|
447
|
+
branch: defaultBranch,
|
|
448
|
+
...(branch !== defaultBranch ? { localBranch: branch } : {}),
|
|
449
|
+
message: `Exported and pushed ${commitSha.slice(0, 8)} to ${link.repoOwner}/${link.repoName} (${defaultBranch}). ` +
|
|
450
|
+
(branch !== defaultBranch
|
|
451
|
+
? `The workspace stays on its local branch ${branch}, which tracks origin/${defaultBranch}. `
|
|
452
|
+
: '') +
|
|
453
|
+
'The workspace is now a git checkout of this repository.',
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
alreadyLinkedResult(linked) {
|
|
457
|
+
return {
|
|
458
|
+
status: 'already_linked',
|
|
459
|
+
repoOwner: linked.repoOwner,
|
|
460
|
+
repoName: linked.repoName,
|
|
461
|
+
repoUrl: linked.repoUrl,
|
|
462
|
+
strategyLinkId: linked.strategyLinkId,
|
|
463
|
+
provider: linked.provider,
|
|
464
|
+
...(linked.cloneUrl ? { cloneUrl: linked.cloneUrl } : {}),
|
|
465
|
+
...(linked.defaultBranch ? { defaultBranch: linked.defaultBranch } : {}),
|
|
466
|
+
message: `This chat is already linked to ${linked.repoOwner}/${linked.repoName}. ` +
|
|
467
|
+
'Exporting again is not possible from this chat — use git add/commit/push in the workspace to publish further changes.',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
linkedPushFailed(err, linkedFields, branch, hint) {
|
|
471
|
+
var _a;
|
|
472
|
+
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);
|
|
473
|
+
return {
|
|
474
|
+
status: 'linked_push_failed',
|
|
475
|
+
error: (0, workspace_git_1.redactSecrets)(raw),
|
|
476
|
+
...linkedFields,
|
|
477
|
+
branch,
|
|
478
|
+
message: hint,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* strategies_link_workspace: attach the chat to an existing strategy link and wire the
|
|
483
|
+
* workspace to its repository with git (clone into an empty workspace, or fetch + new branch
|
|
484
|
+
* when user files exist so nothing is overwritten).
|
|
485
|
+
*/
|
|
486
|
+
async linkWorkspace(args) {
|
|
487
|
+
var _a;
|
|
488
|
+
const strategyLinkId = typeof args.strategy_link_id === 'string' ? args.strategy_link_id.trim() : '';
|
|
489
|
+
if (!strategyLinkId)
|
|
490
|
+
return { statusCode: 400, status: 'failed', error: 'strategy_link_id is required' };
|
|
491
|
+
const sessionId = this.client.sessionId();
|
|
492
|
+
if (!sessionId) {
|
|
493
|
+
return { statusCode: 400, status: 'failed', error: 'The platform token carries no agentSessionId claim; this tool only works inside an Almanak Code session.' };
|
|
494
|
+
}
|
|
495
|
+
const cwd = this.workspaceRoot;
|
|
496
|
+
const res = await this.client.linkSessionRepository(sessionId, { strategyLinkId });
|
|
497
|
+
const failure = this.mapLinkFailure(res);
|
|
498
|
+
if (failure)
|
|
499
|
+
return failure;
|
|
500
|
+
const link = res.data;
|
|
501
|
+
const defaultBranch = link.defaultBranch || 'main';
|
|
502
|
+
const base = {
|
|
503
|
+
provider: link.provider,
|
|
504
|
+
repoId: link.repoId,
|
|
505
|
+
repoOwner: link.repoOwner,
|
|
506
|
+
repoName: link.repoName,
|
|
507
|
+
repoUrl: link.repoUrl,
|
|
508
|
+
cloneUrl: link.cloneUrl,
|
|
509
|
+
strategyLinkId: link.strategyLinkId,
|
|
510
|
+
defaultBranch,
|
|
511
|
+
};
|
|
512
|
+
if (link.status === 'already_linked' && link.strategyLinkId && link.strategyLinkId !== strategyLinkId) {
|
|
513
|
+
return {
|
|
514
|
+
status: 'already_linked',
|
|
515
|
+
...base,
|
|
516
|
+
branch: null,
|
|
517
|
+
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.`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const opts = this.gitOpts(sessionId);
|
|
521
|
+
const netOpts = this.gitOpts(sessionId, workspace_git_1.GIT_NETWORK_TIMEOUT_MS);
|
|
522
|
+
try {
|
|
523
|
+
if (await (0, workspace_git_1.isGitRepo)(cwd)) {
|
|
524
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
525
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
526
|
+
const branch = await (0, workspace_git_1.currentBranch)(opts);
|
|
527
|
+
return {
|
|
528
|
+
status: link.status,
|
|
529
|
+
...base,
|
|
530
|
+
branch,
|
|
531
|
+
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.`,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const userFiles = await (0, workspace_git_1.hasUserFiles)(cwd);
|
|
535
|
+
await (0, workspace_git_1.initRepo)(defaultBranch, opts);
|
|
536
|
+
await (0, workspace_git_1.ensureManagedExcludes)(cwd);
|
|
537
|
+
await (0, workspace_git_1.ensureCommitIdentity)(opts);
|
|
538
|
+
await (0, workspace_git_1.ensureOriginRemote)(link.cloneUrl, opts);
|
|
539
|
+
await (0, workspace_git_1.runGit)(['fetch', '-q', 'origin'], netOpts);
|
|
540
|
+
const remoteHasBranch = await (0, workspace_git_1.remoteBranchExists)(defaultBranch, opts);
|
|
541
|
+
if (!userFiles) {
|
|
542
|
+
if (remoteHasBranch) {
|
|
543
|
+
await (0, workspace_git_1.runGit)(['checkout', '-q', '-B', defaultBranch, `origin/${defaultBranch}`], opts);
|
|
544
|
+
return {
|
|
545
|
+
status: link.status,
|
|
546
|
+
...base,
|
|
547
|
+
branch: defaultBranch,
|
|
548
|
+
message: `Linked ${link.repoOwner}/${link.repoName} and checked out origin/${defaultBranch} into the workspace.`,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
status: link.status,
|
|
553
|
+
...base,
|
|
554
|
+
branch: defaultBranch,
|
|
555
|
+
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}.`,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const sessionBranch = `almanak/session-${sessionId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8)}`;
|
|
559
|
+
await (0, workspace_git_1.runGit)(['symbolic-ref', 'HEAD', `refs/heads/${sessionBranch}`], opts);
|
|
560
|
+
return {
|
|
561
|
+
status: link.status,
|
|
562
|
+
...base,
|
|
563
|
+
branch: sessionBranch,
|
|
564
|
+
message: `Linked ${link.repoOwner}/${link.repoName}. The workspace already had files, so they were NOT overwritten: git was initialised on new branch ${sessionBranch} ` +
|
|
565
|
+
`with the existing files left as uncommitted changes` +
|
|
566
|
+
(remoteHasBranch
|
|
567
|
+
? ` and origin/${defaultBranch} fetched. Reconcile with git diff origin/${defaultBranch}, then commit and push (git push -u origin ${sessionBranch}, or merge into ${defaultBranch}).`
|
|
568
|
+
: `; the repository has no ${defaultBranch} branch yet. Commit and git push -u origin ${defaultBranch} when ready.`),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
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);
|
|
573
|
+
return {
|
|
574
|
+
status: 'linked_attach_failed',
|
|
575
|
+
error: (0, workspace_git_1.redactSecrets)(raw),
|
|
576
|
+
...base,
|
|
577
|
+
branch: null,
|
|
578
|
+
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.`,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
281
582
|
register(mode, definition, handler) {
|
|
282
583
|
this.registeredNames.add(definition.name);
|
|
283
584
|
if (!this.modeEnabled(mode))
|
|
@@ -331,7 +632,7 @@ class PlatformToolHandler {
|
|
|
331
632
|
type: 'object',
|
|
332
633
|
properties: {
|
|
333
634
|
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
|
|
635
|
+
github_strategy_link_id: { type: 'string', description: 'ID of the linked strategy (from strategies_list or the strategies_export_workspace result)' },
|
|
335
636
|
github_ref: { type: 'string', description: 'Git branch, tag, or commit SHA to deploy' },
|
|
336
637
|
github_ref_type: {
|
|
337
638
|
type: 'string',
|
|
@@ -863,92 +1164,56 @@ class PlatformToolHandler {
|
|
|
863
1164
|
return this.client.validateRepository(args.owner, args.repo, ref);
|
|
864
1165
|
});
|
|
865
1166
|
this.register('strategies', {
|
|
866
|
-
name: '
|
|
867
|
-
description: '
|
|
1167
|
+
name: 'strategies_export_workspace',
|
|
1168
|
+
description: 'Export the workspace to a NEW private repository on Almanak Git, the platform-hosted git service, created under the user namespace — there is no choice of git host. ' +
|
|
1169
|
+
'Export the current workspace to a NEW git repository and link it to this chat, immediately, inside this tool call: ' +
|
|
1170
|
+
'the platform creates and links the repository, then this tool runs git init/add/commit/push in the workspace itself. ' +
|
|
1171
|
+
'No widget is shown and no user click is needed — on status "completed" the repository exists with the pushed commit, so ' +
|
|
1172
|
+
'continue in the SAME turn (for example run a backtest or deploy). Pass repository metadata only; never read or submit files. ' +
|
|
1173
|
+
'Session-internal files (.almanak, .almanakdb, almanak_state.db*, .env, .venv, node_modules, .logs) are excluded automatically. ' +
|
|
1174
|
+
'Pass rootDir only when the strategy lives in a subfolder (e.g. "my_strategy/"): its contents are moved up to the workspace root ' +
|
|
1175
|
+
'so the repository is flat. Statuses: "completed" (done: repoUrl, strategyLinkId, commitSha, branch); "already_linked" (this chat ' +
|
|
1176
|
+
'already has a repository — do not export again, use git add/commit/push in the workspace); "name_conflict" (pick a different ' +
|
|
1177
|
+
'repoName and retry); "preflight_failed" (fix the workspace/rootDir and retry); "github_connection_required", "provider_disabled", ' +
|
|
1178
|
+
'"in_progress" (report to the user); "linked_push_failed" (the repository IS linked but the commit/push failed — fix the git ' +
|
|
1179
|
+
'problem and run the git command in the message; do not call this tool again). Never claim the export succeeded before "completed".',
|
|
868
1180
|
inputSchema: {
|
|
869
1181
|
type: 'object',
|
|
870
1182
|
properties: {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1183
|
+
repoName: { type: 'string', description: 'Repository name (required). Lowercase letters, digits, dashes recommended.' },
|
|
1184
|
+
description: { type: 'string', description: 'Repository description.' },
|
|
1185
|
+
rootDir: {
|
|
1186
|
+
type: 'string',
|
|
1187
|
+
description: 'Optional workspace-relative strategy folder whose contents become the repository root (flattened into the workspace). ' +
|
|
1188
|
+
'Omit for strategies at the workspace root (the default). Never pass an absolute path.',
|
|
1189
|
+
},
|
|
875
1190
|
},
|
|
876
|
-
required: [],
|
|
1191
|
+
required: ['repoName'],
|
|
877
1192
|
},
|
|
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
|
-
});
|
|
1193
|
+
}, (args) => this.exportWorkspace(args));
|
|
906
1194
|
this.register('strategies', {
|
|
907
|
-
name: '
|
|
908
|
-
description: '
|
|
1195
|
+
name: 'strategies_link_workspace',
|
|
1196
|
+
description: 'Link an EXISTING strategy repository (a strategy_link_id from strategies_list) to this chat and attach the workspace to it with git, ' +
|
|
1197
|
+
'immediately, inside this tool call. Use it when the user wants to continue work on a strategy they already exported. ' +
|
|
1198
|
+
'An empty workspace is checked out from the repository default branch. A workspace that already has files is NOT overwritten: ' +
|
|
1199
|
+
'the tool initialises git, fetches the repository, and leaves the files as uncommitted changes on a new branch named in the result — ' +
|
|
1200
|
+
'inspect git status / git diff origin/<default branch>, reconcile, then commit and push. If the workspace is already a git ' +
|
|
1201
|
+
'repository, only the origin remote is repointed. Statuses: "linked", "already_linked" (with the linked repo), "failed". ' +
|
|
1202
|
+
'The chat can be linked to exactly one repository; to export a brand new one use strategies_export_workspace instead.',
|
|
909
1203
|
inputSchema: {
|
|
910
1204
|
type: 'object',
|
|
911
1205
|
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.' },
|
|
1206
|
+
strategy_link_id: { type: 'string', description: 'The strategy link ID (from strategies_list).' },
|
|
928
1207
|
},
|
|
929
|
-
required: ['
|
|
1208
|
+
required: ['strategy_link_id'],
|
|
930
1209
|
},
|
|
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
|
-
});
|
|
1210
|
+
}, (args) => this.linkWorkspace(args));
|
|
947
1211
|
this.register('strategies', {
|
|
948
1212
|
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.
|
|
1213
|
+
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is in its ' +
|
|
1214
|
+
'repository (strategies_export_workspace, or git push on a linked chat) and before deploying — backtests run against a ' +
|
|
1215
|
+
'pushed commit, never local files, so export/push first and re-run after every code change you want tested. ' +
|
|
1216
|
+
'The call submits the run, renders a live backtest ' +
|
|
952
1217
|
'widget in the chat, and PAUSES until the run finishes (typically 1-5 minutes); it then resumes you with the ' +
|
|
953
1218
|
'result summary (total return, Sharpe, max drawdown, win rate, trades, and the run_validity verdict) merged into ' +
|
|
954
1219
|
'this tool result as _widgetResponse — or the failure reason if the run failed. A run whose verdict is INVALID or ' +
|
|
@@ -966,7 +1231,7 @@ class PlatformToolHandler {
|
|
|
966
1231
|
properties: {
|
|
967
1232
|
strategy_link_id: {
|
|
968
1233
|
type: 'string',
|
|
969
|
-
description: 'The strategy link ID (from strategies_list or the
|
|
1234
|
+
description: 'The strategy link ID (from strategies_list or the strategies_export_workspace result).',
|
|
970
1235
|
},
|
|
971
1236
|
start_time: {
|
|
972
1237
|
type: 'string',
|
|
@@ -1345,7 +1610,7 @@ class PlatformToolHandler {
|
|
|
1345
1610
|
if (!strategyLinkId) {
|
|
1346
1611
|
return {
|
|
1347
1612
|
statusCode: 400,
|
|
1348
|
-
error: 'strategy_link_id is required — pass the linked strategy id (from strategies_list or the
|
|
1613
|
+
error: 'strategy_link_id is required — pass the linked strategy id (from strategies_list or the strategies_export_workspace result).',
|
|
1349
1614
|
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
1615
|
};
|
|
1351
1616
|
}
|