@cortask/core 0.2.13 → 0.2.15
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/index.d.ts +26 -1
- package/dist/index.js +647 -458
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3048,9 +3048,9 @@ async function ensureBrowser() {
|
|
|
3048
3048
|
async screenshot() {
|
|
3049
3049
|
const tmpPath = `${process.env.TEMP || "/tmp"}/ab-screenshot-${Date.now()}.png`;
|
|
3050
3050
|
await exec2(["screenshot", tmpPath]);
|
|
3051
|
-
const
|
|
3052
|
-
const buf = await
|
|
3053
|
-
await
|
|
3051
|
+
const fs20 = await import("fs/promises");
|
|
3052
|
+
const buf = await fs20.readFile(tmpPath);
|
|
3053
|
+
await fs20.unlink(tmpPath).catch(() => {
|
|
3054
3054
|
});
|
|
3055
3055
|
return buf;
|
|
3056
3056
|
},
|
|
@@ -3655,6 +3655,503 @@ ${lines.join("\n")}` };
|
|
|
3655
3655
|
};
|
|
3656
3656
|
}
|
|
3657
3657
|
|
|
3658
|
+
// src/agent/tools/skill.ts
|
|
3659
|
+
import fs14 from "fs/promises";
|
|
3660
|
+
|
|
3661
|
+
// src/skills/writer.ts
|
|
3662
|
+
import fs12 from "fs/promises";
|
|
3663
|
+
import path12 from "path";
|
|
3664
|
+
|
|
3665
|
+
// src/skills/loader.ts
|
|
3666
|
+
import fs11 from "fs/promises";
|
|
3667
|
+
import path11 from "path";
|
|
3668
|
+
import matter from "gray-matter";
|
|
3669
|
+
|
|
3670
|
+
// src/skills/credential-schema.ts
|
|
3671
|
+
import fs10 from "fs/promises";
|
|
3672
|
+
import path10 from "path";
|
|
3673
|
+
async function loadCredentialSchema(skillDir) {
|
|
3674
|
+
const schemaPath = path10.join(skillDir, "credentials.json");
|
|
3675
|
+
try {
|
|
3676
|
+
const raw = await fs10.readFile(schemaPath, "utf-8");
|
|
3677
|
+
const parsed = JSON.parse(raw);
|
|
3678
|
+
if (!parsed.credentials || !Array.isArray(parsed.credentials)) {
|
|
3679
|
+
return void 0;
|
|
3680
|
+
}
|
|
3681
|
+
return parsed;
|
|
3682
|
+
} catch {
|
|
3683
|
+
return void 0;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function getCredentialStorageKey(skillName, credentialId, fieldKey, storeAs) {
|
|
3687
|
+
if (storeAs) {
|
|
3688
|
+
return `${storeAs}.${fieldKey}`;
|
|
3689
|
+
}
|
|
3690
|
+
return `skill.${skillName}.${credentialId}.${fieldKey}`;
|
|
3691
|
+
}
|
|
3692
|
+
function getInstanceStorageKey(skillName, credentialId, instanceId, fieldKey, storeAs) {
|
|
3693
|
+
if (storeAs) {
|
|
3694
|
+
return `${storeAs}.${instanceId}.${fieldKey}`;
|
|
3695
|
+
}
|
|
3696
|
+
return `skill.${skillName}.${credentialId}.${instanceId}.${fieldKey}`;
|
|
3697
|
+
}
|
|
3698
|
+
function getInstanceRegistryKey(skillName, credentialId, storeAs) {
|
|
3699
|
+
if (storeAs) {
|
|
3700
|
+
return `${storeAs}._instances`;
|
|
3701
|
+
}
|
|
3702
|
+
return `skill.${skillName}.${credentialId}._instances`;
|
|
3703
|
+
}
|
|
3704
|
+
function getOAuth2StorageKeys(skillName, credentialId) {
|
|
3705
|
+
const prefix = `skill.${skillName}.${credentialId}.oauth2`;
|
|
3706
|
+
return {
|
|
3707
|
+
accessToken: `${prefix}.accessToken`,
|
|
3708
|
+
refreshToken: `${prefix}.refreshToken`,
|
|
3709
|
+
expiresAt: `${prefix}.expiresAt`
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// src/skills/loader.ts
|
|
3714
|
+
var skillCache = null;
|
|
3715
|
+
function clearSkillCache() {
|
|
3716
|
+
skillCache = null;
|
|
3717
|
+
}
|
|
3718
|
+
async function loadSkills(bundledDir, userDir, configDirs, credentialStore) {
|
|
3719
|
+
if (skillCache) return skillCache;
|
|
3720
|
+
const skills = [];
|
|
3721
|
+
if (bundledDir) {
|
|
3722
|
+
const entries = await scanSkillDir(bundledDir, "bundled");
|
|
3723
|
+
skills.push(...entries);
|
|
3724
|
+
}
|
|
3725
|
+
if (userDir) {
|
|
3726
|
+
const entries = await scanSkillDir(userDir, "user");
|
|
3727
|
+
skills.push(...entries);
|
|
3728
|
+
}
|
|
3729
|
+
for (const dir of configDirs) {
|
|
3730
|
+
const entries = await scanSkillDir(dir, "config-dir");
|
|
3731
|
+
skills.push(...entries);
|
|
3732
|
+
}
|
|
3733
|
+
for (const skill of skills) {
|
|
3734
|
+
skill.credentialSchema = await loadCredentialSchema(skill.path);
|
|
3735
|
+
const result = await checkEligibility(skill, credentialStore);
|
|
3736
|
+
skill.eligible = result.eligible;
|
|
3737
|
+
skill.ineligibleReason = result.reason;
|
|
3738
|
+
skill.credentialStatus = result.credentialStatus;
|
|
3739
|
+
if (skill.manifest.install) {
|
|
3740
|
+
skill.installOptions = skill.manifest.install.filter((spec) => {
|
|
3741
|
+
if (!spec.os) return true;
|
|
3742
|
+
return spec.os.includes(process.platform);
|
|
3743
|
+
}).map((spec, i) => ({
|
|
3744
|
+
id: spec.id ?? `install-${i}`,
|
|
3745
|
+
kind: spec.kind,
|
|
3746
|
+
label: spec.label ?? `Install via ${spec.kind}`
|
|
3747
|
+
}));
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
skillCache = skills;
|
|
3751
|
+
return skills;
|
|
3752
|
+
}
|
|
3753
|
+
function getEligibleSkills(skills) {
|
|
3754
|
+
return skills.filter((s) => s.eligible);
|
|
3755
|
+
}
|
|
3756
|
+
async function scanSkillDir(dir, source) {
|
|
3757
|
+
const skills = [];
|
|
3758
|
+
try {
|
|
3759
|
+
const entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
3760
|
+
for (const entry of entries) {
|
|
3761
|
+
if (!entry.isDirectory()) continue;
|
|
3762
|
+
const skillDir = path11.join(dir, entry.name);
|
|
3763
|
+
const skillMdPath = path11.join(skillDir, "SKILL.md");
|
|
3764
|
+
try {
|
|
3765
|
+
const raw = await fs11.readFile(skillMdPath, "utf-8");
|
|
3766
|
+
const parsed = matter(raw);
|
|
3767
|
+
const manifest = parsed.data;
|
|
3768
|
+
if (!manifest.name) {
|
|
3769
|
+
manifest.name = entry.name;
|
|
3770
|
+
}
|
|
3771
|
+
if (!manifest.description) {
|
|
3772
|
+
manifest.description = "";
|
|
3773
|
+
}
|
|
3774
|
+
let skillSource = source;
|
|
3775
|
+
try {
|
|
3776
|
+
await fs11.access(path11.join(skillDir, ".origin"));
|
|
3777
|
+
skillSource = "git";
|
|
3778
|
+
} catch {
|
|
3779
|
+
}
|
|
3780
|
+
let hasCodeTools = false;
|
|
3781
|
+
try {
|
|
3782
|
+
await fs11.access(path11.join(skillDir, "index.js"));
|
|
3783
|
+
hasCodeTools = true;
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
skills.push({
|
|
3787
|
+
manifest,
|
|
3788
|
+
content: parsed.content.trim(),
|
|
3789
|
+
path: skillDir,
|
|
3790
|
+
eligible: false,
|
|
3791
|
+
// Will be set by checkEligibility
|
|
3792
|
+
source: skillSource,
|
|
3793
|
+
editable: skillSource !== "bundled",
|
|
3794
|
+
hasCodeTools
|
|
3795
|
+
});
|
|
3796
|
+
} catch {
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
} catch {
|
|
3800
|
+
}
|
|
3801
|
+
return skills;
|
|
3802
|
+
}
|
|
3803
|
+
async function checkEligibility(skill, credentialStore) {
|
|
3804
|
+
const { manifest } = skill;
|
|
3805
|
+
if (manifest.always) {
|
|
3806
|
+
return { eligible: true };
|
|
3807
|
+
}
|
|
3808
|
+
const requiredOs = manifest.compatibility?.os;
|
|
3809
|
+
if (requiredOs && !requiredOs.includes(process.platform)) {
|
|
3810
|
+
return {
|
|
3811
|
+
eligible: false,
|
|
3812
|
+
reason: `Requires OS: ${requiredOs.join(", ")} (current: ${process.platform})`
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
if (manifest.requires?.env) {
|
|
3816
|
+
for (const envVar of manifest.requires.env) {
|
|
3817
|
+
if (!process.env[envVar]) {
|
|
3818
|
+
return {
|
|
3819
|
+
eligible: false,
|
|
3820
|
+
reason: `Missing environment variable: ${envVar}`
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
if (manifest.requires?.bins) {
|
|
3826
|
+
for (const bin of manifest.requires.bins) {
|
|
3827
|
+
if (!await isBinaryAvailable(bin)) {
|
|
3828
|
+
return {
|
|
3829
|
+
eligible: false,
|
|
3830
|
+
reason: `Missing binary: ${bin}`
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
if (skill.credentialSchema && credentialStore) {
|
|
3836
|
+
const credentialStatus = {};
|
|
3837
|
+
let allFulfilled = true;
|
|
3838
|
+
for (const cred of skill.credentialSchema.credentials) {
|
|
3839
|
+
if (cred.type === "oauth2") {
|
|
3840
|
+
const keys = getOAuth2StorageKeys(manifest.name, cred.id);
|
|
3841
|
+
const hasToken = await credentialStore.has(keys.accessToken);
|
|
3842
|
+
credentialStatus[cred.id] = hasToken;
|
|
3843
|
+
if (!hasToken) allFulfilled = false;
|
|
3844
|
+
} else if (cred.multiple && cred.fields) {
|
|
3845
|
+
const registryKey = getInstanceRegistryKey(manifest.name, cred.id, cred.storeAs);
|
|
3846
|
+
const raw = await credentialStore.get(registryKey);
|
|
3847
|
+
const instances = raw ? JSON.parse(raw) : [];
|
|
3848
|
+
let anyInstanceFulfilled = false;
|
|
3849
|
+
for (const instance2 of instances) {
|
|
3850
|
+
let instanceOk = true;
|
|
3851
|
+
for (const field of cred.fields) {
|
|
3852
|
+
if (field.required === false) continue;
|
|
3853
|
+
const key = getInstanceStorageKey(
|
|
3854
|
+
manifest.name,
|
|
3855
|
+
cred.id,
|
|
3856
|
+
instance2.id,
|
|
3857
|
+
field.key,
|
|
3858
|
+
cred.storeAs
|
|
3859
|
+
);
|
|
3860
|
+
if (!await credentialStore.has(key)) {
|
|
3861
|
+
instanceOk = false;
|
|
3862
|
+
break;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
if (instanceOk) {
|
|
3866
|
+
anyInstanceFulfilled = true;
|
|
3867
|
+
break;
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
credentialStatus[cred.id] = anyInstanceFulfilled;
|
|
3871
|
+
if (!anyInstanceFulfilled) allFulfilled = false;
|
|
3872
|
+
} else if (cred.fields) {
|
|
3873
|
+
let fieldsFulfilled = true;
|
|
3874
|
+
for (const field of cred.fields) {
|
|
3875
|
+
if (field.required === false) continue;
|
|
3876
|
+
const key = getCredentialStorageKey(
|
|
3877
|
+
manifest.name,
|
|
3878
|
+
cred.id,
|
|
3879
|
+
field.key,
|
|
3880
|
+
cred.storeAs
|
|
3881
|
+
);
|
|
3882
|
+
const exists = await credentialStore.has(key);
|
|
3883
|
+
if (!exists) {
|
|
3884
|
+
fieldsFulfilled = false;
|
|
3885
|
+
break;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
credentialStatus[cred.id] = fieldsFulfilled;
|
|
3889
|
+
if (!fieldsFulfilled) allFulfilled = false;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
if (!allFulfilled) {
|
|
3893
|
+
return {
|
|
3894
|
+
eligible: false,
|
|
3895
|
+
reason: "Missing required credentials",
|
|
3896
|
+
credentialStatus
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3899
|
+
return { eligible: true, credentialStatus };
|
|
3900
|
+
}
|
|
3901
|
+
return { eligible: true };
|
|
3902
|
+
}
|
|
3903
|
+
async function isBinaryAvailable(name) {
|
|
3904
|
+
const { exec: exec4 } = await import("child_process");
|
|
3905
|
+
const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
|
|
3906
|
+
return new Promise((resolve) => {
|
|
3907
|
+
exec4(cmd, (error) => {
|
|
3908
|
+
resolve(!error);
|
|
3909
|
+
});
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// src/skills/writer.ts
|
|
3914
|
+
var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3915
|
+
function validateSkillName(name) {
|
|
3916
|
+
if (!name || name.length < 2 || name.length > 50) {
|
|
3917
|
+
return "Skill name must be 2-50 characters";
|
|
3918
|
+
}
|
|
3919
|
+
if (!SKILL_NAME_RE.test(name)) {
|
|
3920
|
+
return "Skill name must be kebab-case (lowercase letters, numbers, hyphens)";
|
|
3921
|
+
}
|
|
3922
|
+
return null;
|
|
3923
|
+
}
|
|
3924
|
+
async function createSkill(skillsDir, name, rawContent) {
|
|
3925
|
+
const nameErr = validateSkillName(name);
|
|
3926
|
+
if (nameErr) throw new Error(nameErr);
|
|
3927
|
+
const skillDir = path12.join(skillsDir, name);
|
|
3928
|
+
try {
|
|
3929
|
+
await fs12.access(skillDir);
|
|
3930
|
+
throw new Error(`Skill "${name}" already exists`);
|
|
3931
|
+
} catch (err) {
|
|
3932
|
+
if (err.message.includes("already exists")) throw err;
|
|
3933
|
+
}
|
|
3934
|
+
await fs12.mkdir(skillDir, { recursive: true });
|
|
3935
|
+
await fs12.writeFile(path12.join(skillDir, "SKILL.md"), rawContent, "utf-8");
|
|
3936
|
+
clearSkillCache();
|
|
3937
|
+
return { name, path: skillDir };
|
|
3938
|
+
}
|
|
3939
|
+
async function updateSkill(skillsDir, name, rawContent) {
|
|
3940
|
+
const skillDir = path12.join(skillsDir, name);
|
|
3941
|
+
const skillMdPath = path12.join(skillDir, "SKILL.md");
|
|
3942
|
+
try {
|
|
3943
|
+
await fs12.access(skillMdPath);
|
|
3944
|
+
} catch {
|
|
3945
|
+
throw new Error(`Skill "${name}" not found`);
|
|
3946
|
+
}
|
|
3947
|
+
await fs12.writeFile(skillMdPath, rawContent, "utf-8");
|
|
3948
|
+
clearSkillCache();
|
|
3949
|
+
}
|
|
3950
|
+
async function readSkillFile(skillsDir, name) {
|
|
3951
|
+
const skillMdPath = path12.join(skillsDir, name, "SKILL.md");
|
|
3952
|
+
try {
|
|
3953
|
+
return await fs12.readFile(skillMdPath, "utf-8");
|
|
3954
|
+
} catch {
|
|
3955
|
+
throw new Error(`Skill "${name}" not found`);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// src/skills/installer.ts
|
|
3960
|
+
import { exec as exec3 } from "child_process";
|
|
3961
|
+
import fs13 from "fs/promises";
|
|
3962
|
+
import path13 from "path";
|
|
3963
|
+
async function installSkillFromGit(gitUrl, skillsDir) {
|
|
3964
|
+
await fs13.mkdir(skillsDir, { recursive: true });
|
|
3965
|
+
const repoName = gitUrl.split("/").pop()?.replace(/\.git$/, "");
|
|
3966
|
+
if (!repoName) {
|
|
3967
|
+
throw new Error(`Invalid git URL: ${gitUrl}`);
|
|
3968
|
+
}
|
|
3969
|
+
const targetDir = path13.join(skillsDir, repoName);
|
|
3970
|
+
try {
|
|
3971
|
+
await fs13.access(targetDir);
|
|
3972
|
+
throw new Error(`Skill "${repoName}" is already installed`);
|
|
3973
|
+
} catch (err) {
|
|
3974
|
+
if (err.message.includes("already installed")) throw err;
|
|
3975
|
+
}
|
|
3976
|
+
await execAsync(`git clone --depth 1 "${gitUrl}" "${targetDir}"`);
|
|
3977
|
+
try {
|
|
3978
|
+
await fs13.access(path13.join(targetDir, "SKILL.md"));
|
|
3979
|
+
} catch {
|
|
3980
|
+
await fs13.rm(targetDir, { recursive: true, force: true });
|
|
3981
|
+
throw new Error(
|
|
3982
|
+
`Invalid skill: no SKILL.md found in ${repoName}`
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
await fs13.writeFile(
|
|
3986
|
+
path13.join(targetDir, ".origin"),
|
|
3987
|
+
gitUrl,
|
|
3988
|
+
"utf-8"
|
|
3989
|
+
);
|
|
3990
|
+
try {
|
|
3991
|
+
await fs13.rm(path13.join(targetDir, ".git"), {
|
|
3992
|
+
recursive: true,
|
|
3993
|
+
force: true
|
|
3994
|
+
});
|
|
3995
|
+
} catch {
|
|
3996
|
+
}
|
|
3997
|
+
clearSkillCache();
|
|
3998
|
+
logger.info(`Installed skill "${repoName}" from ${gitUrl}`, "skills");
|
|
3999
|
+
return { name: repoName, path: targetDir };
|
|
4000
|
+
}
|
|
4001
|
+
async function removeSkill(skillName, skillsDir) {
|
|
4002
|
+
const targetDir = path13.join(skillsDir, skillName);
|
|
4003
|
+
try {
|
|
4004
|
+
await fs13.access(targetDir);
|
|
4005
|
+
} catch {
|
|
4006
|
+
throw new Error(`Skill "${skillName}" not found`);
|
|
4007
|
+
}
|
|
4008
|
+
await fs13.rm(targetDir, { recursive: true, force: true });
|
|
4009
|
+
clearSkillCache();
|
|
4010
|
+
logger.info(`Removed skill "${skillName}"`, "skills");
|
|
4011
|
+
}
|
|
4012
|
+
function execAsync(command) {
|
|
4013
|
+
return new Promise((resolve, reject) => {
|
|
4014
|
+
exec3(command, { timeout: 6e4 }, (error, stdout, stderr) => {
|
|
4015
|
+
if (error) {
|
|
4016
|
+
reject(new Error(stderr || error.message));
|
|
4017
|
+
return;
|
|
4018
|
+
}
|
|
4019
|
+
resolve(stdout);
|
|
4020
|
+
});
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/agent/tools/skill.ts
|
|
4025
|
+
function createSkillTool(userSkillsDir, bundledSkillNames) {
|
|
4026
|
+
const bundledNames = new Set(bundledSkillNames);
|
|
4027
|
+
return {
|
|
4028
|
+
definition: {
|
|
4029
|
+
name: "cortask_skill",
|
|
4030
|
+
description: 'Manage custom skills. To create: set action="create", name, and content (full SKILL.md with YAML frontmatter + markdown). To update: action="update" with name and content. To list: action="list". To remove: action="remove" and name.',
|
|
4031
|
+
inputSchema: {
|
|
4032
|
+
type: "object",
|
|
4033
|
+
properties: {
|
|
4034
|
+
action: {
|
|
4035
|
+
type: "string",
|
|
4036
|
+
description: "The action to perform",
|
|
4037
|
+
enum: ["create", "update", "list", "remove"]
|
|
4038
|
+
},
|
|
4039
|
+
name: {
|
|
4040
|
+
type: "string",
|
|
4041
|
+
description: "Skill name in kebab-case (for create/update/remove)"
|
|
4042
|
+
},
|
|
4043
|
+
content: {
|
|
4044
|
+
type: "string",
|
|
4045
|
+
description: "Full SKILL.md content including YAML frontmatter and markdown body (for create/update)"
|
|
4046
|
+
}
|
|
4047
|
+
},
|
|
4048
|
+
required: ["action"]
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
4051
|
+
execute: async (args, _context) => {
|
|
4052
|
+
const action = args.action;
|
|
4053
|
+
try {
|
|
4054
|
+
switch (action) {
|
|
4055
|
+
case "create": {
|
|
4056
|
+
const name = args.name;
|
|
4057
|
+
const content = args.content;
|
|
4058
|
+
if (!name || !content) {
|
|
4059
|
+
return {
|
|
4060
|
+
toolCallId: "",
|
|
4061
|
+
content: "name and content are required for create",
|
|
4062
|
+
isError: true
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
const nameErr = validateSkillName(name);
|
|
4066
|
+
if (nameErr) {
|
|
4067
|
+
return { toolCallId: "", content: nameErr, isError: true };
|
|
4068
|
+
}
|
|
4069
|
+
if (bundledNames.has(name)) {
|
|
4070
|
+
return {
|
|
4071
|
+
toolCallId: "",
|
|
4072
|
+
content: `Cannot create skill "${name}" \u2014 a built-in skill with that name already exists. Choose a different name.`,
|
|
4073
|
+
isError: true
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
const result = await createSkill(userSkillsDir, name, content);
|
|
4077
|
+
return {
|
|
4078
|
+
toolCallId: "",
|
|
4079
|
+
content: `Custom skill "${result.name}" created at ${result.path}. It will be available in the next conversation.`
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
case "update": {
|
|
4083
|
+
const name = args.name;
|
|
4084
|
+
const content = args.content;
|
|
4085
|
+
if (!name || !content) {
|
|
4086
|
+
return {
|
|
4087
|
+
toolCallId: "",
|
|
4088
|
+
content: "name and content are required for update",
|
|
4089
|
+
isError: true
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
await updateSkill(userSkillsDir, name, content);
|
|
4093
|
+
return {
|
|
4094
|
+
toolCallId: "",
|
|
4095
|
+
content: `Custom skill "${name}" updated. Changes will take effect in the next conversation.`
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
case "list": {
|
|
4099
|
+
try {
|
|
4100
|
+
const entries = await fs14.readdir(userSkillsDir, {
|
|
4101
|
+
withFileTypes: true
|
|
4102
|
+
});
|
|
4103
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4104
|
+
if (dirs.length === 0) {
|
|
4105
|
+
return {
|
|
4106
|
+
toolCallId: "",
|
|
4107
|
+
content: "No custom skills found."
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
return {
|
|
4111
|
+
toolCallId: "",
|
|
4112
|
+
content: `Custom skills:
|
|
4113
|
+
${dirs.map((d) => `- ${d}`).join("\n")}`
|
|
4114
|
+
};
|
|
4115
|
+
} catch {
|
|
4116
|
+
return {
|
|
4117
|
+
toolCallId: "",
|
|
4118
|
+
content: "No custom skills found."
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
case "remove": {
|
|
4123
|
+
const name = args.name;
|
|
4124
|
+
if (!name) {
|
|
4125
|
+
return {
|
|
4126
|
+
toolCallId: "",
|
|
4127
|
+
content: "name is required for remove",
|
|
4128
|
+
isError: true
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
await removeSkill(name, userSkillsDir);
|
|
4132
|
+
return {
|
|
4133
|
+
toolCallId: "",
|
|
4134
|
+
content: `Custom skill "${name}" removed.`
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
default:
|
|
4138
|
+
return {
|
|
4139
|
+
toolCallId: "",
|
|
4140
|
+
content: `Unknown action: ${action}`,
|
|
4141
|
+
isError: true
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
} catch (err) {
|
|
4145
|
+
return {
|
|
4146
|
+
toolCallId: "",
|
|
4147
|
+
content: `Skill error: ${err instanceof Error ? err.message : String(err)}`,
|
|
4148
|
+
isError: true
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
|
|
3658
4155
|
// src/agent/tools/index.ts
|
|
3659
4156
|
var builtinTools = [
|
|
3660
4157
|
readFileTool,
|
|
@@ -3673,8 +4170,8 @@ var builtinTools = [
|
|
|
3673
4170
|
|
|
3674
4171
|
// src/credentials/store.ts
|
|
3675
4172
|
import crypto4 from "crypto";
|
|
3676
|
-
import
|
|
3677
|
-
import
|
|
4173
|
+
import fs15 from "fs/promises";
|
|
4174
|
+
import path14 from "path";
|
|
3678
4175
|
function deriveKey(secret, salt) {
|
|
3679
4176
|
return crypto4.scryptSync(secret, salt, 32);
|
|
3680
4177
|
}
|
|
@@ -3719,7 +4216,7 @@ var EncryptedCredentialStore = class {
|
|
|
3719
4216
|
async load() {
|
|
3720
4217
|
if (this.store) return this.store;
|
|
3721
4218
|
try {
|
|
3722
|
-
const raw = await
|
|
4219
|
+
const raw = await fs15.readFile(this.filePath, "utf-8");
|
|
3723
4220
|
this.store = JSON.parse(raw);
|
|
3724
4221
|
} catch {
|
|
3725
4222
|
this.store = { version: 1, entries: {} };
|
|
@@ -3728,10 +4225,10 @@ var EncryptedCredentialStore = class {
|
|
|
3728
4225
|
}
|
|
3729
4226
|
async save() {
|
|
3730
4227
|
if (!this.store) return;
|
|
3731
|
-
await
|
|
4228
|
+
await fs15.mkdir(path14.dirname(this.filePath), { recursive: true });
|
|
3732
4229
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
3733
|
-
await
|
|
3734
|
-
await
|
|
4230
|
+
await fs15.writeFile(tmp, JSON.stringify(this.store, null, 2), "utf-8");
|
|
4231
|
+
await fs15.rename(tmp, this.filePath);
|
|
3735
4232
|
}
|
|
3736
4233
|
async get(key) {
|
|
3737
4234
|
const store = await this.load();
|
|
@@ -3765,13 +4262,13 @@ var EncryptedCredentialStore = class {
|
|
|
3765
4262
|
async function getOrCreateSecret(dataDir) {
|
|
3766
4263
|
const secretFromEnv = process.env.CORTASK_SECRET;
|
|
3767
4264
|
if (secretFromEnv) return secretFromEnv;
|
|
3768
|
-
const keyPath =
|
|
4265
|
+
const keyPath = path14.join(dataDir, "master.key");
|
|
3769
4266
|
try {
|
|
3770
|
-
return await
|
|
4267
|
+
return await fs15.readFile(keyPath, "utf-8");
|
|
3771
4268
|
} catch {
|
|
3772
4269
|
const key = crypto4.randomBytes(32).toString("hex");
|
|
3773
|
-
await
|
|
3774
|
-
await
|
|
4270
|
+
await fs15.mkdir(path14.dirname(keyPath), { recursive: true });
|
|
4271
|
+
await fs15.writeFile(keyPath, key, { mode: 384 });
|
|
3775
4272
|
return key;
|
|
3776
4273
|
}
|
|
3777
4274
|
}
|
|
@@ -3781,8 +4278,8 @@ function credentialKey(category, ...parts) {
|
|
|
3781
4278
|
|
|
3782
4279
|
// src/config/schema.ts
|
|
3783
4280
|
import { z } from "zod";
|
|
3784
|
-
import
|
|
3785
|
-
import
|
|
4281
|
+
import fs16 from "fs/promises";
|
|
4282
|
+
import path15 from "path";
|
|
3786
4283
|
var providerConfigSchema = z.object({
|
|
3787
4284
|
model: z.string().optional()
|
|
3788
4285
|
});
|
|
@@ -3835,7 +4332,7 @@ var cortaskConfigSchema = z.object({
|
|
|
3835
4332
|
});
|
|
3836
4333
|
async function loadConfig(configPath) {
|
|
3837
4334
|
try {
|
|
3838
|
-
const raw = await
|
|
4335
|
+
const raw = await fs16.readFile(configPath, "utf-8");
|
|
3839
4336
|
const resolved = raw.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
3840
4337
|
return process.env[name] ?? "";
|
|
3841
4338
|
});
|
|
@@ -3854,15 +4351,15 @@ async function loadConfig(configPath) {
|
|
|
3854
4351
|
}
|
|
3855
4352
|
}
|
|
3856
4353
|
async function saveConfig(configPath, config) {
|
|
3857
|
-
const dir =
|
|
3858
|
-
await
|
|
4354
|
+
const dir = path15.dirname(configPath);
|
|
4355
|
+
await fs16.mkdir(dir, { recursive: true });
|
|
3859
4356
|
if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
|
|
3860
4357
|
const { stringify } = await import("yaml").catch(() => ({
|
|
3861
4358
|
stringify: (obj) => JSON.stringify(obj, null, 2)
|
|
3862
4359
|
}));
|
|
3863
|
-
await
|
|
4360
|
+
await fs16.writeFile(configPath, stringify(config), "utf-8");
|
|
3864
4361
|
} else {
|
|
3865
|
-
await
|
|
4362
|
+
await fs16.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
3866
4363
|
}
|
|
3867
4364
|
}
|
|
3868
4365
|
function getDataDir() {
|
|
@@ -3870,7 +4367,7 @@ function getDataDir() {
|
|
|
3870
4367
|
return process.env.CORTASK_DATA_DIR;
|
|
3871
4368
|
}
|
|
3872
4369
|
const home = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
3873
|
-
return
|
|
4370
|
+
return path15.join(home, ".cortask");
|
|
3874
4371
|
}
|
|
3875
4372
|
|
|
3876
4373
|
// src/onboarding/validator.ts
|
|
@@ -3915,8 +4412,8 @@ function getDefaultModel(type) {
|
|
|
3915
4412
|
}
|
|
3916
4413
|
|
|
3917
4414
|
// src/workspace/manager.ts
|
|
3918
|
-
import
|
|
3919
|
-
import
|
|
4415
|
+
import fs17 from "fs/promises";
|
|
4416
|
+
import path16 from "path";
|
|
3920
4417
|
import Database2 from "better-sqlite3";
|
|
3921
4418
|
var CORTASK_DIR2 = ".cortask";
|
|
3922
4419
|
var WorkspaceManager = class {
|
|
@@ -3925,7 +4422,7 @@ var WorkspaceManager = class {
|
|
|
3925
4422
|
activeWorkspaceId = null;
|
|
3926
4423
|
constructor(dbPath) {
|
|
3927
4424
|
this.db = new Database2(dbPath);
|
|
3928
|
-
this.dataDir =
|
|
4425
|
+
this.dataDir = path16.dirname(dbPath);
|
|
3929
4426
|
this.db.pragma("journal_mode = WAL");
|
|
3930
4427
|
this.db.pragma("foreign_keys = ON");
|
|
3931
4428
|
this.initSchema();
|
|
@@ -3982,14 +4479,14 @@ var WorkspaceManager = class {
|
|
|
3982
4479
|
async create(name, rootPath) {
|
|
3983
4480
|
const id = crypto.randomUUID();
|
|
3984
4481
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3985
|
-
const absPath = rootPath ?
|
|
3986
|
-
const cortaskDir =
|
|
3987
|
-
await
|
|
3988
|
-
const memoryPath =
|
|
4482
|
+
const absPath = rootPath ? path16.resolve(rootPath) : path16.join(this.dataDir, "projects", id);
|
|
4483
|
+
const cortaskDir = path16.join(absPath, CORTASK_DIR2);
|
|
4484
|
+
await fs17.mkdir(cortaskDir, { recursive: true });
|
|
4485
|
+
const memoryPath = path16.join(cortaskDir, "memory.md");
|
|
3989
4486
|
try {
|
|
3990
|
-
await
|
|
4487
|
+
await fs17.access(memoryPath);
|
|
3991
4488
|
} catch {
|
|
3992
|
-
await
|
|
4489
|
+
await fs17.writeFile(
|
|
3993
4490
|
memoryPath,
|
|
3994
4491
|
"# Project Memory\n\nThis file is used by Cortask to remember important context about this project.\n",
|
|
3995
4492
|
"utf-8"
|
|
@@ -4052,32 +4549,32 @@ var WorkspaceManager = class {
|
|
|
4052
4549
|
return this.get(this.activeWorkspaceId);
|
|
4053
4550
|
}
|
|
4054
4551
|
async readMemory(workspacePath) {
|
|
4055
|
-
const memoryPath =
|
|
4552
|
+
const memoryPath = path16.join(workspacePath, CORTASK_DIR2, "memory.md");
|
|
4056
4553
|
try {
|
|
4057
|
-
return await
|
|
4554
|
+
return await fs17.readFile(memoryPath, "utf-8");
|
|
4058
4555
|
} catch {
|
|
4059
4556
|
return void 0;
|
|
4060
4557
|
}
|
|
4061
4558
|
}
|
|
4062
4559
|
async writeMemory(workspacePath, content) {
|
|
4063
|
-
const memoryPath =
|
|
4064
|
-
await
|
|
4065
|
-
await
|
|
4560
|
+
const memoryPath = path16.join(workspacePath, CORTASK_DIR2, "memory.md");
|
|
4561
|
+
await fs17.mkdir(path16.dirname(memoryPath), { recursive: true });
|
|
4562
|
+
await fs17.writeFile(memoryPath, content, "utf-8");
|
|
4066
4563
|
}
|
|
4067
4564
|
async readGlobalMemory(dataDir) {
|
|
4068
|
-
const memoryPath =
|
|
4565
|
+
const memoryPath = path16.join(dataDir, "memory.md");
|
|
4069
4566
|
try {
|
|
4070
|
-
return await
|
|
4567
|
+
return await fs17.readFile(memoryPath, "utf-8");
|
|
4071
4568
|
} catch {
|
|
4072
4569
|
return void 0;
|
|
4073
4570
|
}
|
|
4074
4571
|
}
|
|
4075
4572
|
async writeGlobalMemory(dataDir, content) {
|
|
4076
|
-
await
|
|
4077
|
-
await
|
|
4573
|
+
await fs17.mkdir(dataDir, { recursive: true });
|
|
4574
|
+
await fs17.writeFile(path16.join(dataDir, "memory.md"), content, "utf-8");
|
|
4078
4575
|
}
|
|
4079
4576
|
getSessionDbPath(workspacePath) {
|
|
4080
|
-
return
|
|
4577
|
+
return path16.join(workspacePath, CORTASK_DIR2, "sessions.db");
|
|
4081
4578
|
}
|
|
4082
4579
|
getChannelWorkspace(chatKey) {
|
|
4083
4580
|
const row = this.db.prepare("SELECT workspace_id FROM channel_workspace_map WHERE chat_key = ?").get(chatKey);
|
|
@@ -4167,366 +4664,118 @@ var SessionStore = class {
|
|
|
4167
4664
|
now
|
|
4168
4665
|
);
|
|
4169
4666
|
}
|
|
4170
|
-
this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
|
|
4171
|
-
});
|
|
4172
|
-
transaction();
|
|
4173
|
-
}
|
|
4174
|
-
getMessages(sessionId) {
|
|
4175
|
-
const rows = this.db.prepare(
|
|
4176
|
-
"SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC"
|
|
4177
|
-
).all(sessionId);
|
|
4178
|
-
return rows.map((r) => ({
|
|
4179
|
-
role: r.role,
|
|
4180
|
-
content: JSON.parse(r.content)
|
|
4181
|
-
}));
|
|
4182
|
-
}
|
|
4183
|
-
deleteSession(id) {
|
|
4184
|
-
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
4185
|
-
}
|
|
4186
|
-
updateTitle(id, title) {
|
|
4187
|
-
this.db.prepare("UPDATE sessions SET title = ? WHERE id = ?").run(title, id);
|
|
4188
|
-
}
|
|
4189
|
-
close() {
|
|
4190
|
-
this.db.close();
|
|
4191
|
-
}
|
|
4192
|
-
};
|
|
4193
|
-
|
|
4194
|
-
// src/session/migrate.ts
|
|
4195
|
-
import Database4 from "better-sqlite3";
|
|
4196
|
-
import fs13 from "fs";
|
|
4197
|
-
function migrateSessionDatabase(dbPath) {
|
|
4198
|
-
if (!fs13.existsSync(dbPath)) {
|
|
4199
|
-
logger.debug(`Session database does not exist yet: ${dbPath}`, "migration");
|
|
4200
|
-
return;
|
|
4201
|
-
}
|
|
4202
|
-
const db = new Database4(dbPath);
|
|
4203
|
-
try {
|
|
4204
|
-
const columns = db.prepare("PRAGMA table_info(sessions)").all();
|
|
4205
|
-
if (columns.length === 0) {
|
|
4206
|
-
logger.debug(`Sessions table does not exist yet in: ${dbPath}`, "migration");
|
|
4207
|
-
db.close();
|
|
4208
|
-
return;
|
|
4209
|
-
}
|
|
4210
|
-
const hasParentSessionId = columns.some((c) => c.name === "parent_session_id");
|
|
4211
|
-
const hasDepth = columns.some((c) => c.name === "depth");
|
|
4212
|
-
const hasChannel = columns.some((c) => c.name === "channel");
|
|
4213
|
-
let changesMade = false;
|
|
4214
|
-
if (!hasParentSessionId) {
|
|
4215
|
-
logger.info(`Migrating database: adding parent_session_id to ${dbPath}`, "migration");
|
|
4216
|
-
db.exec("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
|
|
4217
|
-
changesMade = true;
|
|
4218
|
-
logger.info("\u2713 Added parent_session_id column", "migration");
|
|
4219
|
-
}
|
|
4220
|
-
if (!hasDepth) {
|
|
4221
|
-
logger.info(`Migrating database: adding depth to ${dbPath}`, "migration");
|
|
4222
|
-
db.exec("ALTER TABLE sessions ADD COLUMN depth INTEGER DEFAULT 0");
|
|
4223
|
-
changesMade = true;
|
|
4224
|
-
logger.info("\u2713 Added depth column", "migration");
|
|
4225
|
-
}
|
|
4226
|
-
if (!hasChannel) {
|
|
4227
|
-
logger.info(`Migrating database: adding channel to ${dbPath}`, "migration");
|
|
4228
|
-
db.exec("ALTER TABLE sessions ADD COLUMN channel TEXT");
|
|
4229
|
-
changesMade = true;
|
|
4230
|
-
logger.info("\u2713 Added channel column", "migration");
|
|
4231
|
-
}
|
|
4232
|
-
if (!hasParentSessionId) {
|
|
4233
|
-
db.exec(
|
|
4234
|
-
"CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)"
|
|
4235
|
-
);
|
|
4236
|
-
logger.info("\u2713 Created parent session index", "migration");
|
|
4237
|
-
}
|
|
4238
|
-
if (changesMade) {
|
|
4239
|
-
logger.info(`Migration complete for: ${dbPath}`, "migration");
|
|
4240
|
-
} else {
|
|
4241
|
-
logger.debug(`No migration needed for: ${dbPath}`, "migration");
|
|
4242
|
-
}
|
|
4243
|
-
} catch (err) {
|
|
4244
|
-
logger.error(
|
|
4245
|
-
`Migration failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4246
|
-
"migration"
|
|
4247
|
-
);
|
|
4248
|
-
throw err;
|
|
4249
|
-
} finally {
|
|
4250
|
-
db.close();
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
function migrateAllWorkspaces(workspaces) {
|
|
4254
|
-
logger.info(`Running migrations for ${workspaces.length} workspace(s)`, "migration");
|
|
4255
|
-
let successCount = 0;
|
|
4256
|
-
let errorCount = 0;
|
|
4257
|
-
for (const workspace of workspaces) {
|
|
4258
|
-
const dbPath = `${workspace.rootPath}/.cortask/sessions.db`;
|
|
4259
|
-
try {
|
|
4260
|
-
migrateSessionDatabase(dbPath);
|
|
4261
|
-
successCount++;
|
|
4262
|
-
} catch (err) {
|
|
4263
|
-
errorCount++;
|
|
4264
|
-
logger.error(
|
|
4265
|
-
`Failed to migrate workspace ${workspace.rootPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4266
|
-
"migration"
|
|
4267
|
-
);
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
logger.info(
|
|
4271
|
-
`Migration summary: ${successCount} successful, ${errorCount} failed`,
|
|
4272
|
-
"migration"
|
|
4273
|
-
);
|
|
4274
|
-
if (errorCount > 0) {
|
|
4275
|
-
throw new Error(`Migration failed for ${errorCount} workspace(s)`);
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
|
|
4279
|
-
// src/skills/loader.ts
|
|
4280
|
-
import fs15 from "fs/promises";
|
|
4281
|
-
import path14 from "path";
|
|
4282
|
-
import matter from "gray-matter";
|
|
4283
|
-
|
|
4284
|
-
// src/skills/credential-schema.ts
|
|
4285
|
-
import fs14 from "fs/promises";
|
|
4286
|
-
import path13 from "path";
|
|
4287
|
-
async function loadCredentialSchema(skillDir) {
|
|
4288
|
-
const schemaPath = path13.join(skillDir, "credentials.json");
|
|
4289
|
-
try {
|
|
4290
|
-
const raw = await fs14.readFile(schemaPath, "utf-8");
|
|
4291
|
-
const parsed = JSON.parse(raw);
|
|
4292
|
-
if (!parsed.credentials || !Array.isArray(parsed.credentials)) {
|
|
4293
|
-
return void 0;
|
|
4294
|
-
}
|
|
4295
|
-
return parsed;
|
|
4296
|
-
} catch {
|
|
4297
|
-
return void 0;
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
function getCredentialStorageKey(skillName, credentialId, fieldKey, storeAs) {
|
|
4301
|
-
if (storeAs) {
|
|
4302
|
-
return `${storeAs}.${fieldKey}`;
|
|
4303
|
-
}
|
|
4304
|
-
return `skill.${skillName}.${credentialId}.${fieldKey}`;
|
|
4305
|
-
}
|
|
4306
|
-
function getInstanceStorageKey(skillName, credentialId, instanceId, fieldKey, storeAs) {
|
|
4307
|
-
if (storeAs) {
|
|
4308
|
-
return `${storeAs}.${instanceId}.${fieldKey}`;
|
|
4309
|
-
}
|
|
4310
|
-
return `skill.${skillName}.${credentialId}.${instanceId}.${fieldKey}`;
|
|
4311
|
-
}
|
|
4312
|
-
function getInstanceRegistryKey(skillName, credentialId, storeAs) {
|
|
4313
|
-
if (storeAs) {
|
|
4314
|
-
return `${storeAs}._instances`;
|
|
4315
|
-
}
|
|
4316
|
-
return `skill.${skillName}.${credentialId}._instances`;
|
|
4317
|
-
}
|
|
4318
|
-
function getOAuth2StorageKeys(skillName, credentialId) {
|
|
4319
|
-
const prefix = `skill.${skillName}.${credentialId}.oauth2`;
|
|
4320
|
-
return {
|
|
4321
|
-
accessToken: `${prefix}.accessToken`,
|
|
4322
|
-
refreshToken: `${prefix}.refreshToken`,
|
|
4323
|
-
expiresAt: `${prefix}.expiresAt`
|
|
4324
|
-
};
|
|
4325
|
-
}
|
|
4326
|
-
|
|
4327
|
-
// src/skills/loader.ts
|
|
4328
|
-
var skillCache = null;
|
|
4329
|
-
function clearSkillCache() {
|
|
4330
|
-
skillCache = null;
|
|
4331
|
-
}
|
|
4332
|
-
async function loadSkills(bundledDir, userDir, configDirs, credentialStore) {
|
|
4333
|
-
if (skillCache) return skillCache;
|
|
4334
|
-
const skills = [];
|
|
4335
|
-
if (bundledDir) {
|
|
4336
|
-
const entries = await scanSkillDir(bundledDir, "bundled");
|
|
4337
|
-
skills.push(...entries);
|
|
4338
|
-
}
|
|
4339
|
-
if (userDir) {
|
|
4340
|
-
const entries = await scanSkillDir(userDir, "user");
|
|
4341
|
-
skills.push(...entries);
|
|
4342
|
-
}
|
|
4343
|
-
for (const dir of configDirs) {
|
|
4344
|
-
const entries = await scanSkillDir(dir, "config-dir");
|
|
4345
|
-
skills.push(...entries);
|
|
4346
|
-
}
|
|
4347
|
-
for (const skill of skills) {
|
|
4348
|
-
skill.credentialSchema = await loadCredentialSchema(skill.path);
|
|
4349
|
-
const result = await checkEligibility(skill, credentialStore);
|
|
4350
|
-
skill.eligible = result.eligible;
|
|
4351
|
-
skill.ineligibleReason = result.reason;
|
|
4352
|
-
skill.credentialStatus = result.credentialStatus;
|
|
4353
|
-
if (skill.manifest.install) {
|
|
4354
|
-
skill.installOptions = skill.manifest.install.filter((spec) => {
|
|
4355
|
-
if (!spec.os) return true;
|
|
4356
|
-
return spec.os.includes(process.platform);
|
|
4357
|
-
}).map((spec, i) => ({
|
|
4358
|
-
id: spec.id ?? `install-${i}`,
|
|
4359
|
-
kind: spec.kind,
|
|
4360
|
-
label: spec.label ?? `Install via ${spec.kind}`
|
|
4361
|
-
}));
|
|
4362
|
-
}
|
|
4363
|
-
}
|
|
4364
|
-
skillCache = skills;
|
|
4365
|
-
return skills;
|
|
4366
|
-
}
|
|
4367
|
-
function getEligibleSkills(skills) {
|
|
4368
|
-
return skills.filter((s) => s.eligible);
|
|
4369
|
-
}
|
|
4370
|
-
async function scanSkillDir(dir, source) {
|
|
4371
|
-
const skills = [];
|
|
4372
|
-
try {
|
|
4373
|
-
const entries = await fs15.readdir(dir, { withFileTypes: true });
|
|
4374
|
-
for (const entry of entries) {
|
|
4375
|
-
if (!entry.isDirectory()) continue;
|
|
4376
|
-
const skillDir = path14.join(dir, entry.name);
|
|
4377
|
-
const skillMdPath = path14.join(skillDir, "SKILL.md");
|
|
4378
|
-
try {
|
|
4379
|
-
const raw = await fs15.readFile(skillMdPath, "utf-8");
|
|
4380
|
-
const parsed = matter(raw);
|
|
4381
|
-
const manifest = parsed.data;
|
|
4382
|
-
if (!manifest.name) {
|
|
4383
|
-
manifest.name = entry.name;
|
|
4384
|
-
}
|
|
4385
|
-
if (!manifest.description) {
|
|
4386
|
-
manifest.description = "";
|
|
4387
|
-
}
|
|
4388
|
-
let skillSource = source;
|
|
4389
|
-
try {
|
|
4390
|
-
await fs15.access(path14.join(skillDir, ".origin"));
|
|
4391
|
-
skillSource = "git";
|
|
4392
|
-
} catch {
|
|
4393
|
-
}
|
|
4394
|
-
let hasCodeTools = false;
|
|
4395
|
-
try {
|
|
4396
|
-
await fs15.access(path14.join(skillDir, "index.js"));
|
|
4397
|
-
hasCodeTools = true;
|
|
4398
|
-
} catch {
|
|
4399
|
-
}
|
|
4400
|
-
skills.push({
|
|
4401
|
-
manifest,
|
|
4402
|
-
content: parsed.content.trim(),
|
|
4403
|
-
path: skillDir,
|
|
4404
|
-
eligible: false,
|
|
4405
|
-
// Will be set by checkEligibility
|
|
4406
|
-
source: skillSource,
|
|
4407
|
-
editable: skillSource !== "bundled",
|
|
4408
|
-
hasCodeTools
|
|
4409
|
-
});
|
|
4410
|
-
} catch {
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
} catch {
|
|
4667
|
+
this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
|
|
4668
|
+
});
|
|
4669
|
+
transaction();
|
|
4414
4670
|
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4671
|
+
getMessages(sessionId) {
|
|
4672
|
+
const rows = this.db.prepare(
|
|
4673
|
+
"SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC"
|
|
4674
|
+
).all(sessionId);
|
|
4675
|
+
return rows.map((r) => ({
|
|
4676
|
+
role: r.role,
|
|
4677
|
+
content: JSON.parse(r.content)
|
|
4678
|
+
}));
|
|
4421
4679
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
return {
|
|
4425
|
-
eligible: false,
|
|
4426
|
-
reason: `Requires OS: ${requiredOs.join(", ")} (current: ${process.platform})`
|
|
4427
|
-
};
|
|
4680
|
+
deleteSession(id) {
|
|
4681
|
+
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
4428
4682
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
if (!process.env[envVar]) {
|
|
4432
|
-
return {
|
|
4433
|
-
eligible: false,
|
|
4434
|
-
reason: `Missing environment variable: ${envVar}`
|
|
4435
|
-
};
|
|
4436
|
-
}
|
|
4437
|
-
}
|
|
4683
|
+
updateTitle(id, title) {
|
|
4684
|
+
this.db.prepare("UPDATE sessions SET title = ? WHERE id = ?").run(title, id);
|
|
4438
4685
|
}
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
if (!await isBinaryAvailable(bin)) {
|
|
4442
|
-
return {
|
|
4443
|
-
eligible: false,
|
|
4444
|
-
reason: `Missing binary: ${bin}`
|
|
4445
|
-
};
|
|
4446
|
-
}
|
|
4447
|
-
}
|
|
4686
|
+
close() {
|
|
4687
|
+
this.db.close();
|
|
4448
4688
|
}
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
if (field.required === false) continue;
|
|
4467
|
-
const key = getInstanceStorageKey(
|
|
4468
|
-
manifest.name,
|
|
4469
|
-
cred.id,
|
|
4470
|
-
instance2.id,
|
|
4471
|
-
field.key,
|
|
4472
|
-
cred.storeAs
|
|
4473
|
-
);
|
|
4474
|
-
if (!await credentialStore.has(key)) {
|
|
4475
|
-
instanceOk = false;
|
|
4476
|
-
break;
|
|
4477
|
-
}
|
|
4478
|
-
}
|
|
4479
|
-
if (instanceOk) {
|
|
4480
|
-
anyInstanceFulfilled = true;
|
|
4481
|
-
break;
|
|
4482
|
-
}
|
|
4483
|
-
}
|
|
4484
|
-
credentialStatus[cred.id] = anyInstanceFulfilled;
|
|
4485
|
-
if (!anyInstanceFulfilled) allFulfilled = false;
|
|
4486
|
-
} else if (cred.fields) {
|
|
4487
|
-
let fieldsFulfilled = true;
|
|
4488
|
-
for (const field of cred.fields) {
|
|
4489
|
-
if (field.required === false) continue;
|
|
4490
|
-
const key = getCredentialStorageKey(
|
|
4491
|
-
manifest.name,
|
|
4492
|
-
cred.id,
|
|
4493
|
-
field.key,
|
|
4494
|
-
cred.storeAs
|
|
4495
|
-
);
|
|
4496
|
-
const exists = await credentialStore.has(key);
|
|
4497
|
-
if (!exists) {
|
|
4498
|
-
fieldsFulfilled = false;
|
|
4499
|
-
break;
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
|
-
credentialStatus[cred.id] = fieldsFulfilled;
|
|
4503
|
-
if (!fieldsFulfilled) allFulfilled = false;
|
|
4504
|
-
}
|
|
4689
|
+
};
|
|
4690
|
+
|
|
4691
|
+
// src/session/migrate.ts
|
|
4692
|
+
import Database4 from "better-sqlite3";
|
|
4693
|
+
import fs18 from "fs";
|
|
4694
|
+
function migrateSessionDatabase(dbPath) {
|
|
4695
|
+
if (!fs18.existsSync(dbPath)) {
|
|
4696
|
+
logger.debug(`Session database does not exist yet: ${dbPath}`, "migration");
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
const db = new Database4(dbPath);
|
|
4700
|
+
try {
|
|
4701
|
+
const columns = db.prepare("PRAGMA table_info(sessions)").all();
|
|
4702
|
+
if (columns.length === 0) {
|
|
4703
|
+
logger.debug(`Sessions table does not exist yet in: ${dbPath}`, "migration");
|
|
4704
|
+
db.close();
|
|
4705
|
+
return;
|
|
4505
4706
|
}
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
};
|
|
4707
|
+
const hasParentSessionId = columns.some((c) => c.name === "parent_session_id");
|
|
4708
|
+
const hasDepth = columns.some((c) => c.name === "depth");
|
|
4709
|
+
const hasChannel = columns.some((c) => c.name === "channel");
|
|
4710
|
+
let changesMade = false;
|
|
4711
|
+
if (!hasParentSessionId) {
|
|
4712
|
+
logger.info(`Migrating database: adding parent_session_id to ${dbPath}`, "migration");
|
|
4713
|
+
db.exec("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
|
|
4714
|
+
changesMade = true;
|
|
4715
|
+
logger.info("\u2713 Added parent_session_id column", "migration");
|
|
4512
4716
|
}
|
|
4513
|
-
|
|
4717
|
+
if (!hasDepth) {
|
|
4718
|
+
logger.info(`Migrating database: adding depth to ${dbPath}`, "migration");
|
|
4719
|
+
db.exec("ALTER TABLE sessions ADD COLUMN depth INTEGER DEFAULT 0");
|
|
4720
|
+
changesMade = true;
|
|
4721
|
+
logger.info("\u2713 Added depth column", "migration");
|
|
4722
|
+
}
|
|
4723
|
+
if (!hasChannel) {
|
|
4724
|
+
logger.info(`Migrating database: adding channel to ${dbPath}`, "migration");
|
|
4725
|
+
db.exec("ALTER TABLE sessions ADD COLUMN channel TEXT");
|
|
4726
|
+
changesMade = true;
|
|
4727
|
+
logger.info("\u2713 Added channel column", "migration");
|
|
4728
|
+
}
|
|
4729
|
+
if (!hasParentSessionId) {
|
|
4730
|
+
db.exec(
|
|
4731
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)"
|
|
4732
|
+
);
|
|
4733
|
+
logger.info("\u2713 Created parent session index", "migration");
|
|
4734
|
+
}
|
|
4735
|
+
if (changesMade) {
|
|
4736
|
+
logger.info(`Migration complete for: ${dbPath}`, "migration");
|
|
4737
|
+
} else {
|
|
4738
|
+
logger.debug(`No migration needed for: ${dbPath}`, "migration");
|
|
4739
|
+
}
|
|
4740
|
+
} catch (err) {
|
|
4741
|
+
logger.error(
|
|
4742
|
+
`Migration failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4743
|
+
"migration"
|
|
4744
|
+
);
|
|
4745
|
+
throw err;
|
|
4746
|
+
} finally {
|
|
4747
|
+
db.close();
|
|
4514
4748
|
}
|
|
4515
|
-
return { eligible: true };
|
|
4516
4749
|
}
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4750
|
+
function migrateAllWorkspaces(workspaces) {
|
|
4751
|
+
logger.info(`Running migrations for ${workspaces.length} workspace(s)`, "migration");
|
|
4752
|
+
let successCount = 0;
|
|
4753
|
+
let errorCount = 0;
|
|
4754
|
+
for (const workspace of workspaces) {
|
|
4755
|
+
const dbPath = `${workspace.rootPath}/.cortask/sessions.db`;
|
|
4756
|
+
try {
|
|
4757
|
+
migrateSessionDatabase(dbPath);
|
|
4758
|
+
successCount++;
|
|
4759
|
+
} catch (err) {
|
|
4760
|
+
errorCount++;
|
|
4761
|
+
logger.error(
|
|
4762
|
+
`Failed to migrate workspace ${workspace.rootPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4763
|
+
"migration"
|
|
4764
|
+
);
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
logger.info(
|
|
4768
|
+
`Migration summary: ${successCount} successful, ${errorCount} failed`,
|
|
4769
|
+
"migration"
|
|
4770
|
+
);
|
|
4771
|
+
if (errorCount > 0) {
|
|
4772
|
+
throw new Error(`Migration failed for ${errorCount} workspace(s)`);
|
|
4773
|
+
}
|
|
4525
4774
|
}
|
|
4526
4775
|
|
|
4527
4776
|
// src/skills/tools.ts
|
|
4528
|
-
import
|
|
4529
|
-
import
|
|
4777
|
+
import fs19 from "fs/promises";
|
|
4778
|
+
import path17 from "path";
|
|
4530
4779
|
async function buildSkillTools(skills, credentialStore) {
|
|
4531
4780
|
const toolDefs = [];
|
|
4532
4781
|
const toolNames = /* @__PURE__ */ new Set();
|
|
@@ -4655,13 +4904,13 @@ async function executeHttpTool(template, args, skill, credentialStore, workspace
|
|
|
4655
4904
|
isError: true
|
|
4656
4905
|
};
|
|
4657
4906
|
}
|
|
4658
|
-
const tempDir =
|
|
4659
|
-
await
|
|
4907
|
+
const tempDir = path17.join(workspacePath, "_temp");
|
|
4908
|
+
await fs19.mkdir(tempDir, { recursive: true });
|
|
4660
4909
|
const isJson = looksLikeJson(content);
|
|
4661
4910
|
const ext = isJson ? "json" : "txt";
|
|
4662
4911
|
const filename = `${template.name}_${Date.now()}.${ext}`;
|
|
4663
|
-
const filePath =
|
|
4664
|
-
await
|
|
4912
|
+
const filePath = path17.join(tempDir, filename);
|
|
4913
|
+
await fs19.writeFile(filePath, content, "utf-8");
|
|
4665
4914
|
const summary = buildResponseSummary(content, isJson);
|
|
4666
4915
|
return {
|
|
4667
4916
|
toolCallId: "",
|
|
@@ -4795,71 +5044,6 @@ function resolvePlaceholders(template, args, credValues) {
|
|
|
4795
5044
|
});
|
|
4796
5045
|
}
|
|
4797
5046
|
|
|
4798
|
-
// src/skills/installer.ts
|
|
4799
|
-
import { exec as exec3 } from "child_process";
|
|
4800
|
-
import fs17 from "fs/promises";
|
|
4801
|
-
import path16 from "path";
|
|
4802
|
-
async function installSkillFromGit(gitUrl, skillsDir) {
|
|
4803
|
-
await fs17.mkdir(skillsDir, { recursive: true });
|
|
4804
|
-
const repoName = gitUrl.split("/").pop()?.replace(/\.git$/, "");
|
|
4805
|
-
if (!repoName) {
|
|
4806
|
-
throw new Error(`Invalid git URL: ${gitUrl}`);
|
|
4807
|
-
}
|
|
4808
|
-
const targetDir = path16.join(skillsDir, repoName);
|
|
4809
|
-
try {
|
|
4810
|
-
await fs17.access(targetDir);
|
|
4811
|
-
throw new Error(`Skill "${repoName}" is already installed`);
|
|
4812
|
-
} catch (err) {
|
|
4813
|
-
if (err.message.includes("already installed")) throw err;
|
|
4814
|
-
}
|
|
4815
|
-
await execAsync(`git clone --depth 1 "${gitUrl}" "${targetDir}"`);
|
|
4816
|
-
try {
|
|
4817
|
-
await fs17.access(path16.join(targetDir, "SKILL.md"));
|
|
4818
|
-
} catch {
|
|
4819
|
-
await fs17.rm(targetDir, { recursive: true, force: true });
|
|
4820
|
-
throw new Error(
|
|
4821
|
-
`Invalid skill: no SKILL.md found in ${repoName}`
|
|
4822
|
-
);
|
|
4823
|
-
}
|
|
4824
|
-
await fs17.writeFile(
|
|
4825
|
-
path16.join(targetDir, ".origin"),
|
|
4826
|
-
gitUrl,
|
|
4827
|
-
"utf-8"
|
|
4828
|
-
);
|
|
4829
|
-
try {
|
|
4830
|
-
await fs17.rm(path16.join(targetDir, ".git"), {
|
|
4831
|
-
recursive: true,
|
|
4832
|
-
force: true
|
|
4833
|
-
});
|
|
4834
|
-
} catch {
|
|
4835
|
-
}
|
|
4836
|
-
clearSkillCache();
|
|
4837
|
-
logger.info(`Installed skill "${repoName}" from ${gitUrl}`, "skills");
|
|
4838
|
-
return { name: repoName, path: targetDir };
|
|
4839
|
-
}
|
|
4840
|
-
async function removeSkill(skillName, skillsDir) {
|
|
4841
|
-
const targetDir = path16.join(skillsDir, skillName);
|
|
4842
|
-
try {
|
|
4843
|
-
await fs17.access(targetDir);
|
|
4844
|
-
} catch {
|
|
4845
|
-
throw new Error(`Skill "${skillName}" not found`);
|
|
4846
|
-
}
|
|
4847
|
-
await fs17.rm(targetDir, { recursive: true, force: true });
|
|
4848
|
-
clearSkillCache();
|
|
4849
|
-
logger.info(`Removed skill "${skillName}"`, "skills");
|
|
4850
|
-
}
|
|
4851
|
-
function execAsync(command) {
|
|
4852
|
-
return new Promise((resolve, reject) => {
|
|
4853
|
-
exec3(command, { timeout: 6e4 }, (error, stdout, stderr) => {
|
|
4854
|
-
if (error) {
|
|
4855
|
-
reject(new Error(stderr || error.message));
|
|
4856
|
-
return;
|
|
4857
|
-
}
|
|
4858
|
-
resolve(stdout);
|
|
4859
|
-
});
|
|
4860
|
-
});
|
|
4861
|
-
}
|
|
4862
|
-
|
|
4863
5047
|
// src/skills/oauth2.ts
|
|
4864
5048
|
function buildSkillOAuth2AuthUrl(skillName, oauth, clientId, redirectUri, state) {
|
|
4865
5049
|
const params = new URLSearchParams({
|
|
@@ -5594,6 +5778,8 @@ export {
|
|
|
5594
5778
|
createBrowserTool,
|
|
5595
5779
|
createCronTool,
|
|
5596
5780
|
createProvider,
|
|
5781
|
+
createSkill,
|
|
5782
|
+
createSkillTool,
|
|
5597
5783
|
createSubagentTool,
|
|
5598
5784
|
createSwitchWorkspaceTool,
|
|
5599
5785
|
credentialKey,
|
|
@@ -5614,12 +5800,15 @@ export {
|
|
|
5614
5800
|
logger,
|
|
5615
5801
|
migrateAllWorkspaces,
|
|
5616
5802
|
migrateSessionDatabase,
|
|
5803
|
+
readSkillFile,
|
|
5617
5804
|
registerSubagentRun,
|
|
5618
5805
|
removeSkill,
|
|
5619
5806
|
revokeSkillOAuth2,
|
|
5620
5807
|
saveConfig,
|
|
5621
5808
|
setSubagentRunner,
|
|
5809
|
+
updateSkill,
|
|
5622
5810
|
validateCronExpr,
|
|
5623
|
-
validateProvider
|
|
5811
|
+
validateProvider,
|
|
5812
|
+
validateSkillName
|
|
5624
5813
|
};
|
|
5625
5814
|
//# sourceMappingURL=index.js.map
|